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>.
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|yyyy)(HH)(MM) (cksum) (fname)
43
my $regex_nonzipinfo_line = qr"^\s*(\d+)\s+(\S+)\s+(\d+)\s+(-?\d+\%)\s+(\d?\d)-(\d?\d)-(\d+)\s+(\d?\d):(\d\d)\s+([0-9a-f]+)\s\s(.*)$";
49
die "uzip: missing command and/or archive arguments\n" if ($#ARGV < 1);
51
# Initialization of some global variables
53
my %known = ( './' => 1 );
55
my $oldpwd = POSIX::getcwd();
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($) {
65
$fname =~ s,(^|/)(?:\.?\./)+,$1,;
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($) {
76
if (!defined($zipfs_realpathname_table)) {
77
$zipfs_realpathname_table = {};
78
if (!open(ZIP, "$cmd_list $qarchive |")) {
81
foreach my $line (<ZIP>) {
83
if ($op_has_zipinfo) {
84
if ($line =~ $regex_zipinfo_line) {
86
$zipfs_realpathname_table->{zipfs_canonicalize_pathname($fname)} = $fname;
89
if ($line =~ $regex_nonzipinfo_line) {
91
$zipfs_realpathname_table->{zipfs_canonicalize_pathname($fname)} = $fname;
99
if (exists($zipfs_realpathname_table->{$fname})) {
100
return $zipfs_realpathname_table->{$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
116
# Remove a file from the archive.
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");
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.
134
my ($qfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
135
&checkargs(1, 'archive directory', @_);
136
&safesystem("$cmd_delete $qarchive $qfile/ >/dev/null", 12);
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);
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
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");
174
chdir $oldpwd || &croak("chdir $oldpwd failed");
175
rmdir $tmpdir || &croak("rmdir $tmpdir failed");
179
# Add an empty directory the the archive.
180
# This is similar to mczipfs_copyin, except that we don't need
184
&checkargs(1, 'directory', @_);
185
my ($qdir) = quotemeta $dir;
187
my $tmpdir = &mktmpdir();
188
chdir $tmpdir || &croak("chdir $tmpdir failed");
190
&safesystem("$cmd_add $aqarchive $qdir >/dev/null");
192
chdir $oldpwd || &croak("chdir $oldpwd failed");
193
rmdir $tmpdir || &croak("rmdir $tmpdir failed");
197
# Add a link to the archive. This operation is not used yet,
198
# because it is not supported by the MC extfs.
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");
213
chdir $oldpwd || &croak("chdir $oldpwd failed");
214
rmdir $tmpdir || &croak("rmdir $tmpdir failed");
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");
231
# Use unzip to find the link destination of a certain file in the
233
sub get_link_destination {
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 '');
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.
248
open (PIPE, "$cmd_list $qarchive |") || &croak("$app_unzip failed");
249
if ($op_has_zipinfo) {
254
next if /^Empty zipfile\.$/;
255
my @match = /$regex_zipinfo_line/;
256
next if ($#match != 13);
257
&checked_print_file(@match);
262
my @match = /$regex_nonzipinfo_line/;
263
next if ($#match != 10);
264
my @rmatch = ('', '', 'unknown', $match[0], '', $match[2], $match[1],
265
$match[6] > 100 ? $match[6] : $match[6] + ($match[6] < 70 ? 2000 : 1900), $match[4], $match[5],
266
$match[7], $match[8], "00", $match[10]);
267
&checked_print_file(@rmatch);
271
&croak("$app_unzip failed") if ($! != 0);
272
&croak("$app_unzip failed", 'non-zero exit status ('.($? >> 8).')')
275
foreach my $key (sort keys %pending) {
276
foreach my $file (@{ $pending{$key} }) {
277
&print_file(@{ $file });
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.
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");
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};
329
push @{$pending{$dirname}}, $item;
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.
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--');
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";
353
# Die with a reasonable error message.
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 '.'.
364
my ($dirs, $mode) = @_;
365
$dirs = &cleandirs($dirs);
366
return if ($dirs eq '.');
369
while (($newpos = index($dirs, '/', $newpos+1)) != -1) {
370
my $dir = substr($dirs, 0, $newpos);
371
mkdir ($dir, $mode) || &croak("mkdir $dir failed");
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 '.'.
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");
393
# Return a semi-canonical directory name.
401
# Make a temporary directory with mode 0700.
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.
411
my ($file, $pwd) = @_;
412
return "$pwd/$file" if ($file !~ /^\//);
416
# Like the system built-in function, but with error checking.
417
# The other argument is an exit status to allow.
419
my ($command, @allowrc) = @_;
420
my ($desc) = ($command =~ /^([^ ]*) */);
421
$desc = File::Basename::basename($desc);
424
&croak("`$desc' failed") if (($rc & 0xFF) != 0);
427
foreach my $arc (@allowrc) {
428
return if ($rc == $arc);
430
&croak("`$desc' failed", "non-zero exit status ($rc)");
434
# Like backticks built-in, but with error checking.
436
my ($command, @allowrc) = @_;
437
my ($desc) = ($command =~ /^([^ ]*) /);
438
$desc = File::Basename::basename($desc);
439
my $out = `$command`;
441
&croak("`$desc' failed") if (($rc & 0xFF) != 0);
444
foreach my $arc (@allowrc) {
445
return if ($rc == $arc);
447
&croak("`$desc' failed", "non-zero exit status ($rc)");
452
# Make sure enough arguments are supplied, or die.
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.
465
for (my $c = 0; $c < length $name; $c++) {
466
my $ch = substr($name, $c, 1);
467
$out .= '\\' if (index('*?[]\\', $ch) != -1);
470
return quotemeta($out);