Document "Only directories" checkbox
[midnight-commander/osp/petrarad.git] / vfs / extfs / uzip.in
blob6d6e2b1af66c288bcb70c41a75811ade32877e38
1 #! @PERL@ -w
3 # zip file archive Virtual File System for Midnight Commander
4 # Version 1.4.0 (2001-08-07).
6 # (C) 2000-2001  Oskar Liljeblad <osk@hem.passagen.se>.
9 use POSIX;
10 use File::Basename;
11 use strict;
14 # Configuration options
17 # Location of the zip program
18 my $app_zip = "@ZIP@";
19 # Location of the unzip program
20 my $app_unzip = "@UNZIP@";
21 # Set this to 1 if zipinfo (unzip -Z) is to be used (recommended), otherwise 0.
22 my $op_has_zipinfo = @HAVE_ZIPINFO@;
24 # Command used to list archives (zipinfo mode)
25 my $cmd_list_zi = "$app_unzip -Z -l -T";
26 # Command used to list archives (non-zipinfo mode)
27 my $cmd_list_nzi = "$app_unzip -qq -v";
28 # Command used to add a file to the archive
29 my $cmd_add = "$app_zip -g";
30 # Command used to add a link file to the archive (unused)
31 my $cmd_addlink = "$app_zip -g -y";
32 # Command used to delete a file from the archive
33 my $cmd_delete = "$app_zip -d";
34 # Command used to extract a file to standard out
35 my $cmd_extract = "$app_unzip -p";
37 # -rw-r--r--  2.2 unx     2891 tx     1435 defN 20000330.211927 ./edit.html
38 # (perm) (?) (?) (size) (?) (zippedsize) (method) (yyyy)(mm)(dd)(HH)(MM) (fname)
39 my $regex_zipinfo_line = qr"^(\S{7,10})\s+(\d+\.\d+)\s+(\S+)\s+(\d+)\s+(\S\S)\s+(\d+)\s+(\S{4})\s+(\d{4})(\d\d)(\d\d)\.(\d\d)(\d\d)(\d\d)\s(.*)$";
41 #     2891  Defl:N     1435  50%  03-30-00 21:19  50cbaaf8  ./edit.html
42 # (size) (method) (zippedsize) (zipratio) (mm)(dd)(yy)(HH)(MM) (cksum) (fname)
43 my $regex_nonzipinfo_line = qr"^\s*(\d+)\s+(\S+)\s+(\d+)\s+(-?\d+\%)\s+(\d?\d)-(\d?\d)-(\d\d)\s+(\d?\d):(\d\d)\s+([0-9a-f]+)\s\s(.*)$";
46 # Main code
49 die "uzip: missing command and/or archive arguments\n" if ($#ARGV < 1);
51 # Initialization of some global variables
52 my $cmd = shift;
53 my %known = ( './' => 1 );
54 my %pending = ();
55 my $oldpwd = POSIX::getcwd();
56 my $archive = shift;
57 my $aarchive = absolutize($archive, $oldpwd);
58 my $cmd_list = ($op_has_zipinfo ? $cmd_list_zi : $cmd_list_nzi);
59 my ($qarchive, $aqarchive) = map (quotemeta, $archive, $aarchive);
61 # Strip all "." and ".." path components from a pathname.
62 sub zipfs_canonicalize_pathname($) {
63   my ($fname) = @_;
64   $fname =~ s,/+,/,g;
65   $fname =~ s,(^|/)(?:\.?\./)+,$1,;
66   return $fname;
69 # The Midnight Commander never calls this script with archive pathnames
70 # starting with either "./" or "../". Some ZIP files contain such names,
71 # so we need to build a translation table for them.
72 my $zipfs_realpathname_table = undef;
73 sub zipfs_realpathname($) {
74     my ($fname) = @_;
76     if (!defined($zipfs_realpathname_table)) {
77         $zipfs_realpathname_table = {};
78         if (!open(ZIP, "$cmd_list $qarchive |")) {
79             return $fname;
80         }
81         foreach my $line (<ZIP>) {
82             $line =~ s/\r*\n*$//;
83             if ($op_has_zipinfo) {
84                 if ($line =~ $regex_zipinfo_line) {
85                     my ($fname) = ($14);
86                     $zipfs_realpathname_table->{zipfs_canonicalize_pathname($fname)} = $fname;
87                 }
88             } else {
89                 if ($line =~ $regex_nonzipinfo_line) {
90                     my ($fname) = ($11);
91                     $zipfs_realpathname_table->{zipfs_canonicalize_pathname($fname)} = $fname;
92                 }
93             }
94         }
95         if (!close(ZIP)) {
96             return $fname;
97         }
98     }
99     if (exists($zipfs_realpathname_table->{$fname})) {
100         return $zipfs_realpathname_table->{$fname};
101     }
102     return $fname;
105 if ($cmd eq 'list')    { &mczipfs_list(@ARGV); }
106 if ($cmd eq 'rm')      { &mczipfs_rm(@ARGV); }
107 if ($cmd eq 'rmdir')   { &mczipfs_rmdir(@ARGV); }
108 if ($cmd eq 'mkdir')   { &mczipfs_mkdir(@ARGV); }
109 if ($cmd eq 'copyin')  { &mczipfs_copyin(@ARGV); }
110 if ($cmd eq 'copyout') { &mczipfs_copyout(@ARGV); }
111 if ($cmd eq 'run')               { &mczipfs_run(@ARGV); }
112 #if ($cmd eq 'mklink')  { &mczipfs_mklink(@ARGV); }             # Not supported by MC extfs
113 #if ($cmd eq 'linkout') { &mczipfs_linkout(@ARGV); }    # Not supported by MC extfs
114 exit 1;
116 # Remove a file from the archive.
117 sub mczipfs_rm {
118         my ($qfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
120         # "./" at the beginning of pathnames is stripped by Info-ZIP,
121         # so convert it to "[.]/" to prevent stripping.
122         $qfile =~ s/^\\\./[.]/;
124         &checkargs(1, 'archive file', @_);
125         &safesystem("$cmd_delete $qarchive $qfile >/dev/null");
126         exit;
129 # Remove an empty directory from the archive.
130 # The only difference from mczipfs_rm is that we append an 
131 # additional slash to the directory name to remove. I am not
132 # sure this is absolutely necessary, but it doesn't hurt.
133 sub mczipfs_rmdir {
134         my ($qfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
135         &checkargs(1, 'archive directory', @_);
136         &safesystem("$cmd_delete $qarchive $qfile/ >/dev/null", 12);
137   exit;
140 # Extract a file from the archive.
141 # Note that we don't need to check if the file is a link,
142 # because mc apparently doesn't call copyout for symbolic links.
143 sub mczipfs_copyout {
144         my ($qafile, $qfsfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
145         &checkargs(1, 'archive file', @_);
146         &checkargs(2, 'local file', @_);
147         &safesystem("$cmd_extract $qarchive $qafile > $qfsfile", 11);
148   exit;
151 # Add a file to the archive.
152 # This is done by making a temporary directory, in which
153 # we create a symlink the original file (with a new name).
154 # Zip is then run to include the real file in the archive,
155 # with the name of the symbolic link.
156 # Here we also doesn't need to check for symbolic links,
157 # because the mc extfs doesn't allow adding of symbolic
158 # links.
159 sub mczipfs_copyin {
160         my ($afile, $fsfile) = @_;
161         &checkargs(1, 'archive file', @_);
162         &checkargs(2, 'local file', @_);
163         my ($qafile) = quotemeta $afile;
164         $fsfile = &absolutize($fsfile, $oldpwd);
165         my $adir = File::Basename::dirname($afile);
167         my $tmpdir = &mktmpdir();
168         chdir $tmpdir || &croak("chdir $tmpdir failed");
169         &mkdirs($adir, 0700);
170         symlink ($fsfile, $afile) || &croak("link $afile failed");
171         &safesystem("$cmd_add $aqarchive $qafile >/dev/null");
172         unlink $afile || &croak("unlink $afile failed");
173         &rmdirs($adir);
174         chdir $oldpwd || &croak("chdir $oldpwd failed");
175         rmdir $tmpdir || &croak("rmdir $tmpdir failed");
176   exit;
179 # Add an empty directory the the archive.
180 # This is similar to mczipfs_copyin, except that we don't need
181 # to use symlinks.
182 sub mczipfs_mkdir {
183         my ($dir) = @_;
184         &checkargs(1, 'directory', @_);
185         my ($qdir) = quotemeta $dir;
187         my $tmpdir = &mktmpdir();
188         chdir $tmpdir || &croak("chdir $tmpdir failed");
189         &mkdirs($dir, 0700);
190         &safesystem("$cmd_add $aqarchive $qdir >/dev/null");
191         &rmdirs($dir);
192         chdir $oldpwd || &croak("chdir $oldpwd failed");
193         rmdir $tmpdir || &croak("rmdir $tmpdir failed");
194   exit;
197 # Add a link to the archive. This operation is not used yet,
198 # because it is not supported by the MC extfs.
199 sub mczipfs_mklink {
200         my ($linkdest, $afile) = @_;
201         &checkargs(1, 'link destination', @_);
202         &checkargs(2, 'archive file', @_);
203         my ($qafile) = quotemeta $afile;
204         my $adir = File::Basename::dirname($afile);
206         my $tmpdir = &mktmpdir();
207         chdir $tmpdir || &croak("chdir $tmpdir failed");
208         &mkdirs($adir, 0700);
209         symlink ($linkdest, $afile) || &croak("link $afile failed");
210         &safesystem("$cmd_addlink $aqarchive $qafile >/dev/null");
211         unlink $afile || &croak("unlink $afile failed");
212         &rmdirs($adir);
213         chdir $oldpwd || &croak("chdir $oldpwd failed");
214         rmdir $tmpdir || &croak("rmdir $tmpdir failed");
215   exit;
218 # This operation is not used yet, because it is not
219 # supported by the MC extfs.
220 sub mczipfs_linkout {
221         my ($afile, $fsfile) = @_;
222         &checkargs(1, 'archive file', @_);
223         &checkargs(2, 'local file', @_);
224         my ($qafile) = map { &zipquotemeta($_) } $afile;
226         my $linkdest = &get_link_destination($afile);
227         symlink ($linkdest, $fsfile) || &croak("link $fsfile failed");
228   exit;
231 # Use unzip to find the link destination of a certain file in the
232 # archive.
233 sub get_link_destination {
234         my ($afile) = @_;
235         my ($qafile) = map { &zipquotemeta($_) } $afile;
236         my $linkdest = safeticks("$cmd_extract $qarchive $qafile");
237         &croak ("extract failed", "link destination of $afile not found")
238                         if (!defined $linkdest || $linkdest eq '');
239         return $linkdest;
242 # List files in the archive.
243 # Because mc currently doesn't allow a file's parent directory
244 # to be listed after the file itself, we need to do some
245 # rearranging of the output. Most of this is done in
246 # checked_print_file.
247 sub mczipfs_list {
248         open (PIPE, "$cmd_list $qarchive |") || &croak("$app_unzip failed");
249         if ($op_has_zipinfo) {
250                 while (<PIPE>) {
251                         chomp;
252                         next if /^Archive:/;
253                         next if /^\d+ file/;
254                         next if /^Empty zipfile\.$/;
255                         my @match = /$regex_zipinfo_line/;
256                         next if ($#match != 13);
257                         &checked_print_file(@match);
258                 }
259         } else {
260                 while (<PIPE>) {
261                         chomp;
262                         my @match = /$regex_nonzipinfo_line/;
263                         next if ($#match != 10);
264                         my @rmatch = ('', '', 'unknown', $match[0], '', $match[2], $match[1],
265                                         $match[6] + ($match[6] < 70 ? 2000 : 1900), $match[4], $match[5],
266                                         $match[7], $match[8], "00", $match[10]);
267                         &checked_print_file(@rmatch);
268                 }
269         }
270         if (!close (PIPE)) {
271                 &croak("$app_unzip failed") if ($! != 0);
272                 &croak("$app_unzip failed", 'non-zero exit status ('.($? >> 8).')') 
273         }
275         foreach my $key (sort keys %pending) {
276                 foreach my $file (@{ $pending{$key} }) {
277                         &print_file(@{ $file });
278                 }
279         }
281   exit;
284 # Execute a file in the archive, by first extracting it to a
285 # temporary directory. The name of the extracted file will be
286 # the same as the name of it in the archive.
287 sub mczipfs_run {
288         my ($afile) = @_;
289         &checkargs(1, 'archive file', @_);
290         my $qafile = &zipquotemeta(zipfs_realpathname($afile));
291         my $tmpdir = &mktmpdir();
292         my $tmpfile = File::Basename::basename($afile);
294         chdir $tmpdir || &croak("chdir $tmpdir failed");
295         &safesystem("$cmd_extract $aqarchive $qafile > $tmpfile");
296   chmod 0700, $tmpfile;
297         &safesystem("./$tmpfile");
298         unlink $tmpfile || &croak("rm $tmpfile failed");
299         chdir $oldpwd || &croak("chdir $oldpwd failed");
300         rmdir $tmpdir || &croak("rmdir $tmpdir failed");
301   exit;
304 # This is called prior to printing the listing of a file.
305 # A check is done to see if the parent directory of the file has already
306 # been printed or not. If it hasn't, we must cache it (in %pending) and
307 # print it later once the parent directory has been listed. When all
308 # files have been processed, there may still be some that haven't been 
309 # printed because their parent directories weren't listed at all. These
310 # files are dealt with in mczipfs_list.
311 sub checked_print_file {
312         my @waiting = ([ @_ ]);
314         while ($#waiting != -1) {
315                 my $item = shift @waiting;
316                 my $filename = ${$item}[13];
317                 my $dirname = File::Basename::dirname($filename) . '/';
319                 if (exists $known{$dirname}) {
320                         &print_file(@{$item});
321                         if ($filename =~ /\/$/) {
322                                 $known{$filename} = 1;
323                                 if (exists $pending{$filename}) {
324                                         push @waiting, @{ $pending{$filename} };
325                                         delete $pending{$filename};
326                                 }
327                         }
328                 } else {
329                         push @{$pending{$dirname}}, $item;
330                 }
331         }
334 # Print the mc extfs listing of a file from a set of parsed fields.
335 # If the file is a link, we extract it from the zip archive and
336 # include the output as the link destination. Because this output
337 # is not newline terminated, we must execute unzip once for each
338 # link file encountered.
339 sub print_file {
340         my ($perms,$zipver,$platform,$realsize,$format,$cmpsize,$comp,$year,$mon,$day,$hours,$mins,$secs,$filename) = @_;
341         if ($platform ne 'unx') {
342                 $perms = ($filename =~ /\/$/ ? 'drwxr-xr-x' : '-rw-r--r--');
343         }
344         printf "%-10s    1 %-8d %-8d %8s %s/%s/%s %s:%s:%s %s", $perms, $<,
345                 $(, $realsize, $mon, $day, $year, $hours, $mins, $secs, $filename;
346         if ($platform eq 'unx' && $perms =~ /^l/) {
347                 my $linkdest = &get_link_destination($filename);
348                 print " -> $linkdest";
349         }
350         print "\n";
353 # Die with a reasonable error message.
354 sub croak {
355         my ($command, $desc) = @_;
356         die "uzip ($cmd): $command - $desc\n" if (defined $desc);
357         die "uzip ($cmd): $command - $!\n";
360 # Make a set of directories, like the command `mkdir -p'.
361 # This subroutine has been tailored for this script, and
362 # because of that, it ignored the directory name '.'.
363 sub mkdirs {
364         my ($dirs, $mode) = @_;
365         $dirs = &cleandirs($dirs);
366         return if ($dirs eq '.');
368         my $newpos = -1;
369         while (($newpos = index($dirs, '/', $newpos+1)) != -1) {
370                 my $dir = substr($dirs, 0, $newpos);
371                 mkdir ($dir, $mode) || &croak("mkdir $dir failed");
372         }
373         mkdir ($dirs, $mode) || &croak("mkdir $dirs failed");
376 # Remove a set of directories, failing if the directories
377 # contain other files.
378 # This subroutine has been tailored for this script, and
379 # because of that, it ignored the directory name '.'.
380 sub rmdirs {
381         my ($dirs) = @_;
382         $dirs = &cleandirs($dirs);
383         return if ($dirs eq '.');
385         rmdir $dirs || &croak("rmdir $dirs failed");
386         my $newpos = length($dirs);
387         while (($newpos = rindex($dirs, '/', $newpos-1)) != -1) {
388                 my $dir = substr($dirs, 0, $newpos);
389                 rmdir $dir || &croak("rmdir $dir failed");
390         }
393 # Return a semi-canonical directory name.
394 sub cleandirs {
395         my ($dir) = @_;
396         $dir =~ s:/+:/:g;
397         $dir =~ s:/*$::;
398         return $dir;
401 # Make a temporary directory with mode 0700.
402 sub mktmpdir {
403         use File::Temp qw(mkdtemp);
404         my $template = "/tmp/mcuzipfs.XXXXXX";
405         $template="$ENV{MC_TMPDIR}/mcuzipfs.XXXXXX" if ($ENV{MC_TMPDIR});
406         return mkdtemp($template);
409 # Make a filename absolute and return it.
410 sub absolutize {
411         my ($file, $pwd) = @_;
412         return "$pwd/$file" if ($file !~ /^\//);
413         return $file;
416 # Like the system built-in function, but with error checking.
417 # The other argument is an exit status to allow.
418 sub safesystem {
419         my ($command, @allowrc) = @_;
420         my ($desc) = ($command =~ /^([^ ]*) */);
421         $desc = File::Basename::basename($desc);
422         system $command;
423         my $rc = $?;
424         &croak("`$desc' failed") if (($rc & 0xFF) != 0);
425         if ($rc != 0) {
426                 $rc = $rc >> 8;
427                 foreach my $arc (@allowrc) {
428                         return if ($rc == $arc);
429                 }
430                 &croak("`$desc' failed", "non-zero exit status ($rc)");
431         }
434 # Like backticks built-in, but with error checking.
435 sub safeticks {
436         my ($command, @allowrc) = @_;
437         my ($desc) = ($command =~ /^([^ ]*) /);
438         $desc = File::Basename::basename($desc);
439         my $out = `$command`;
440         my $rc = $?;
441         &croak("`$desc' failed") if (($rc & 0xFF) != 0);
442         if ($rc != 0) {
443                 $rc = $rc >> 8;
444                 foreach my $arc (@allowrc) {
445                         return if ($rc == $arc);
446                 }
447                 &croak("`$desc' failed", "non-zero exit status ($rc)");
448         }
449         return $out;
452 # Make sure enough arguments are supplied, or die.
453 sub checkargs {
454         my $count = shift;
455         my $desc = shift;
456         &croak('missing argument', $desc) if ($count-1 > $#_);
459 # Quote zip wildcard metacharacters. Unfortunately Info-ZIP zip and unzip
460 # on unix interpret some wildcards in filenames, despite the fact that
461 # the shell already does this. Thus this function.
462 sub zipquotemeta {
463         my ($name) = @_;
464         my $out = '';
465         for (my $c = 0; $c < length $name; $c++) {
466                 my $ch = substr($name, $c, 1);
467                 $out .= '\\' if (index('*?[]\\', $ch) != -1);
468                 $out .= $ch;
469         }
470         return quotemeta($out);