summaryrefslogtreecommitdiffstats
path: root/src/vfs/extfs/helpers/uzip.in
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/vfs/extfs/helpers/uzip.in483
1 files changed, 483 insertions, 0 deletions
diff --git a/src/vfs/extfs/helpers/uzip.in b/src/vfs/extfs/helpers/uzip.in
new file mode 100644
index 0000000..ceffb53
--- /dev/null
+++ b/src/vfs/extfs/helpers/uzip.in
@@ -0,0 +1,483 @@
+#! @PERL@
+#
+# zip file archive Virtual File System for Midnight Commander
+# Version 1.4.0 (2001-08-07).
+#
+# (C) 2000-2001 Oskar Liljeblad <osk@hem.passagen.se>.
+#
+
+use POSIX;
+use File::Basename;
+use strict;
+use warnings;
+
+#
+# Configuration options
+#
+
+# Location of the zip program
+my $app_zip = "@ZIP@";
+# Location of the unzip program
+my $app_unzip = $ENV{MC_TEST_EXTFS_LIST_CMD} || "@UNZIP@";
+# Set this to 1 if zipinfo (unzip -Z) is to be used (recommended), otherwise 0.
+my $op_has_zipinfo = exists($ENV{MC_TEST_EXTFS_HAVE_ZIPINFO}) ? $ENV{MC_TEST_EXTFS_HAVE_ZIPINFO} : @HAVE_ZIPINFO@;
+
+# Command used to list archives (zipinfo mode)
+my $cmd_list_zi = "$app_unzip -Z -l -T";
+# Command used to list archives (non-zipinfo mode)
+my $cmd_list_nzi = "$app_unzip -qq -v";
+# Command used to add a file to the archive
+my $cmd_add = "$app_zip -g";
+# Command used to add a link file to the archive (unused)
+my $cmd_addlink = "$app_zip -g -y";
+# Command used to delete a file from the archive
+my $cmd_delete = "$app_zip -d";
+# Command used to extract a file to standard out
+my $cmd_extract = "$app_unzip -p";
+
+# -rw-r--r-- 2.2 unx 2891 tx 1435 defN 20000330.211927 ./edit.html
+# (perm) (?) (?) (size) (?) (zippedsize) (method) (yyyy)(mm)(dd).(HH)(MM)(SS) (fname)
+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(.*)$";
+
+# 2891 Defl:N 1435 50% 03-30-00 21:19 50cbaaf8 ./edit.html
+# (size) (method) (zippedsize) (zipratio) (mm)-(dd)-(yy|yyyy) (HH):(MM) (cksum) (fname)
+# or: (yyyy)-(mm)-(dd)
+my $regex_nonzipinfo_line = qr"^\s*(\d+)\s+(\S+)\s+(\d+)\s+(-?\d+\%)\s+(\d+)-(\d?\d)-(\d+)\s+(\d?\d):(\d\d)\s+([0-9a-f]+)\s\s(.*)$";
+
+#
+# Main code
+#
+
+die "uzip: missing command and/or archive arguments\n" if ($#ARGV < 1);
+
+# Initialization of some global variables
+my $cmd = shift;
+my %known = ( './' => 1 );
+my %pending = ();
+my $oldpwd = POSIX::getcwd();
+my $archive = shift;
+my $aarchive = absolutize($archive, $oldpwd);
+my $cmd_list = ($op_has_zipinfo ? $cmd_list_zi : $cmd_list_nzi);
+my ($qarchive, $aqarchive) = map (quotemeta, $archive, $aarchive);
+
+# Strip all "." and ".." path components from a pathname.
+sub zipfs_canonicalize_pathname($) {
+ my ($fname) = @_;
+ $fname =~ s,/+,/,g;
+ $fname =~ s,(^|/)(?:\.?\./)+,$1,;
+ return $fname;
+}
+
+# The Midnight Commander never calls this script with archive pathnames
+# starting with either "./" or "../". Some ZIP files contain such names,
+# so we need to build a translation table for them.
+my $zipfs_realpathname_table = undef;
+sub zipfs_realpathname($) {
+ my ($fname) = @_;
+
+ if (!defined($zipfs_realpathname_table)) {
+ $zipfs_realpathname_table = {};
+ if (!open(ZIP, "$cmd_list $qarchive |")) {
+ return $fname;
+ }
+ foreach my $line (<ZIP>) {
+ $line =~ s/\r*\n*$//;
+ if ($op_has_zipinfo) {
+ if ($line =~ $regex_zipinfo_line) {
+ my ($fname) = ($14);
+ $zipfs_realpathname_table->{zipfs_canonicalize_pathname($fname)} = $fname;
+ }
+ } else {
+ if ($line =~ $regex_nonzipinfo_line) {
+ my ($fname) = ($11);
+ $zipfs_realpathname_table->{zipfs_canonicalize_pathname($fname)} = $fname;
+ }
+ }
+ }
+ if (!close(ZIP)) {
+ return $fname;
+ }
+ }
+ if (exists($zipfs_realpathname_table->{$fname})) {
+ return $zipfs_realpathname_table->{$fname};
+ }
+ return $fname;
+}
+
+if ($cmd eq 'list') { &mczipfs_list(@ARGV); }
+if ($cmd eq 'rm') { &mczipfs_rm(@ARGV); }
+if ($cmd eq 'rmdir') { &mczipfs_rmdir(@ARGV); }
+if ($cmd eq 'mkdir') { &mczipfs_mkdir(@ARGV); }
+if ($cmd eq 'copyin') { &mczipfs_copyin(@ARGV); }
+if ($cmd eq 'copyout') { &mczipfs_copyout(@ARGV); }
+if ($cmd eq 'run') { &mczipfs_run(@ARGV); }
+#if ($cmd eq 'mklink') { &mczipfs_mklink(@ARGV); } # Not supported by MC extfs
+#if ($cmd eq 'linkout') { &mczipfs_linkout(@ARGV); } # Not supported by MC extfs
+exit 1;
+
+# Remove a file from the archive.
+sub mczipfs_rm {
+ my ($qfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
+
+ # "./" at the beginning of pathnames is stripped by Info-ZIP,
+ # so convert it to "[.]/" to prevent stripping.
+ $qfile =~ s/^\\\./[.]/;
+
+ &checkargs(1, 'archive file', @_);
+ &safesystem("$cmd_delete $qarchive $qfile >/dev/null");
+ exit;
+}
+
+# Remove an empty directory from the archive.
+# The only difference from mczipfs_rm is that we append an
+# additional slash to the directory name to remove. I am not
+# sure this is absolutely necessary, but it doesn't hurt.
+sub mczipfs_rmdir {
+ my ($qfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
+ &checkargs(1, 'archive directory', @_);
+ &safesystem("$cmd_delete $qarchive $qfile/ >/dev/null", 12);
+ exit;
+}
+
+# Extract a file from the archive.
+# Note that we don't need to check if the file is a link,
+# because mc apparently doesn't call copyout for symbolic links.
+sub mczipfs_copyout {
+ my ($qafile, $qfsfile) = map { &zipquotemeta(zipfs_realpathname($_)) } @_;
+ &checkargs(1, 'archive file', @_);
+ &checkargs(2, 'local file', @_);
+ &safesystem("$cmd_extract $qarchive $qafile > $qfsfile", 11);
+ exit;
+}
+
+# Add a file to the archive.
+# This is done by making a temporary directory, in which
+# we create a symlink the original file (with a new name).
+# Zip is then run to include the real file in the archive,
+# with the name of the symbolic link.
+# Here we also doesn't need to check for symbolic links,
+# because the mc extfs doesn't allow adding of symbolic
+# links.
+sub mczipfs_copyin {
+ my ($afile, $fsfile) = @_;
+ &checkargs(1, 'archive file', @_);
+ &checkargs(2, 'local file', @_);
+ my ($qafile) = quotemeta $afile;
+ $fsfile = &absolutize($fsfile, $oldpwd);
+ my $adir = File::Basename::dirname($afile);
+
+ my $tmpdir = &mktmpdir();
+ chdir $tmpdir || &croak("chdir $tmpdir failed");
+ &mkdirs($adir, 0700);
+ symlink ($fsfile, $afile) || &croak("link $afile failed");
+ &safesystem("$cmd_add $aqarchive $qafile >/dev/null");
+ unlink $afile || &croak("unlink $afile failed");
+ &rmdirs($adir);
+ chdir $oldpwd || &croak("chdir $oldpwd failed");
+ rmdir $tmpdir || &croak("rmdir $tmpdir failed");
+ exit;
+}
+
+# Add an empty directory the the archive.
+# This is similar to mczipfs_copyin, except that we don't need
+# to use symlinks.
+sub mczipfs_mkdir {
+ my ($dir) = @_;
+ &checkargs(1, 'directory', @_);
+ my ($qdir) = quotemeta $dir;
+
+ my $tmpdir = &mktmpdir();
+ chdir $tmpdir || &croak("chdir $tmpdir failed");
+ &mkdirs($dir, 0700);
+ &safesystem("$cmd_add $aqarchive $qdir >/dev/null");
+ &rmdirs($dir);
+ chdir $oldpwd || &croak("chdir $oldpwd failed");
+ rmdir $tmpdir || &croak("rmdir $tmpdir failed");
+ exit;
+}
+
+# Add a link to the archive. This operation is not used yet,
+# because it is not supported by the MC extfs.
+sub mczipfs_mklink {
+ my ($linkdest, $afile) = @_;
+ &checkargs(1, 'link destination', @_);
+ &checkargs(2, 'archive file', @_);
+ my ($qafile) = quotemeta $afile;
+ my $adir = File::Basename::dirname($afile);
+
+ my $tmpdir = &mktmpdir();
+ chdir $tmpdir || &croak("chdir $tmpdir failed");
+ &mkdirs($adir, 0700);
+ symlink ($linkdest, $afile) || &croak("link $afile failed");
+ &safesystem("$cmd_addlink $aqarchive $qafile >/dev/null");
+ unlink $afile || &croak("unlink $afile failed");
+ &rmdirs($adir);
+ chdir $oldpwd || &croak("chdir $oldpwd failed");
+ rmdir $tmpdir || &croak("rmdir $tmpdir failed");
+ exit;
+}
+
+# This operation is not used yet, because it is not
+# supported by the MC extfs.
+sub mczipfs_linkout {
+ my ($afile, $fsfile) = @_;
+ &checkargs(1, 'archive file', @_);
+ &checkargs(2, 'local file', @_);
+ my ($qafile) = map { &zipquotemeta($_) } $afile;
+
+ my $linkdest = &get_link_destination($afile);
+ symlink ($linkdest, $fsfile) || &croak("link $fsfile failed");
+ exit;
+}
+
+# Use unzip to find the link destination of a certain file in the
+# archive.
+sub get_link_destination {
+ my ($afile) = @_;
+ my ($qafile) = map { &zipquotemeta($_) } $afile;
+ my $linkdest = safeticks("$cmd_extract $qarchive $qafile");
+ &croak ("extract failed", "link destination of $afile not found")
+ if (!defined $linkdest || $linkdest eq '');
+ return $linkdest;
+}
+
+# List files in the archive.
+# Because mc currently doesn't allow a file's parent directory
+# to be listed after the file itself, we need to do some
+# rearranging of the output. Most of this is done in
+# checked_print_file.
+sub mczipfs_list {
+ open (PIPE, "$cmd_list $qarchive |") || &croak("$app_unzip failed");
+ if ($op_has_zipinfo) {
+ while (<PIPE>) {
+ chomp;
+ next if /^Archive:/;
+ next if /^\d+ file/;
+ next if /^Empty zipfile\.$/;
+ my @match = /$regex_zipinfo_line/;
+ next if ($#match != 13);
+ &checked_print_file(@match);
+ }
+ } else {
+ while (<PIPE>) {
+ chomp;
+ my @match = /$regex_nonzipinfo_line/;
+ next if ($#match != 10);
+
+ # Massage the date.
+ my ($year, $month, $day) = $match[4] > 12
+ ? ($match[4], $match[5], $match[6]) # 4,5,6 = Y,M,D
+ : ($match[6], $match[4], $match[5]); # 4,5,6 = M,D,Y
+ $year += ($year < 70 ? 2000 : 1900) if $year < 100; # Fix 2-digit year.
+
+ my @rmatch = ('', '', 'unknown', $match[0], '', $match[2], $match[1],
+ $year, $month, $day, $match[7], $match[8], "00", $match[10]);
+ &checked_print_file(@rmatch);
+ }
+ }
+ if (!close (PIPE)) {
+ &croak("$app_unzip failed") if ($! != 0);
+ &croak("$app_unzip failed", 'non-zero exit status ('.($? >> 8).')')
+ }
+
+ foreach my $key (sort keys %pending) {
+ foreach my $file (@{ $pending{$key} }) {
+ &print_file(@{ $file });
+ }
+ }
+
+ exit;
+}
+
+# Execute a file in the archive, by first extracting it to a
+# temporary directory. The name of the extracted file will be
+# the same as the name of it in the archive.
+sub mczipfs_run {
+ my ($afile) = @_;
+ &checkargs(1, 'archive file', @_);
+ my $qafile = &zipquotemeta(zipfs_realpathname($afile));
+ my $tmpdir = &mktmpdir();
+ my $tmpfile = File::Basename::basename($afile);
+
+ chdir $tmpdir || &croak("chdir $tmpdir failed");
+ &safesystem("$cmd_extract $aqarchive $qafile > $tmpfile");
+ chmod 0700, $tmpfile;
+ &safesystem("./$tmpfile");
+ unlink $tmpfile || &croak("rm $tmpfile failed");
+ chdir $oldpwd || &croak("chdir $oldpwd failed");
+ rmdir $tmpdir || &croak("rmdir $tmpdir failed");
+ exit;
+}
+
+# This is called prior to printing the listing of a file.
+# A check is done to see if the parent directory of the file has already
+# been printed or not. If it hasn't, we must cache it (in %pending) and
+# print it later once the parent directory has been listed. When all
+# files have been processed, there may still be some that haven't been
+# printed because their parent directories weren't listed at all. These
+# files are dealt with in mczipfs_list.
+sub checked_print_file {
+ my @waiting = ([ @_ ]);
+
+ while ($#waiting != -1) {
+ my $item = shift @waiting;
+ my $filename = ${$item}[13];
+ my $dirname = File::Basename::dirname($filename) . '/';
+
+ if (exists $known{$dirname}) {
+ &print_file(@{$item});
+ if ($filename =~ /\/$/) {
+ $known{$filename} = 1;
+ if (exists $pending{$filename}) {
+ push @waiting, @{ $pending{$filename} };
+ delete $pending{$filename};
+ }
+ }
+ } else {
+ push @{$pending{$dirname}}, $item;
+ }
+ }
+}
+
+# Print the mc extfs listing of a file from a set of parsed fields.
+# If the file is a link, we extract it from the zip archive and
+# include the output as the link destination. Because this output
+# is not newline terminated, we must execute unzip once for each
+# link file encountered.
+sub print_file {
+ my ($perms,$zipver,$platform,$realsize,$format,$cmpsize,$comp,$year,$mon,$day,$hours,$mins,$secs,$filename) = @_;
+ if ($platform ne 'unx') {
+ $perms = ($filename =~ /\/$/ ? 'drwxr-xr-x' : '-rw-r--r--');
+ }
+ # adjust abnormal perms on directory
+ if ($platform eq 'unx' && $filename =~ /\/$/ && $perms =~ /^\?(.*)$/) {
+ $perms = 'd'.$1;
+ }
+ printf "%-10s 1 %-8d %-8d %8s %s/%s/%s %s:%s:%s ./%s", $perms, $<,
+ $(, $realsize, $mon, $day, $year, $hours, $mins, $secs, $filename;
+ if ($platform eq 'unx' && $perms =~ /^l/) {
+ my $linkdest = &get_link_destination($filename);
+ print " -> $linkdest";
+ }
+ print "\n";
+}
+
+# Die with a reasonable error message.
+sub croak {
+ my ($command, $desc) = @_;
+ die "uzip ($cmd): $command - $desc\n" if (defined $desc);
+ die "uzip ($cmd): $command - $!\n";
+}
+
+# Make a set of directories, like the command `mkdir -p'.
+# This subroutine has been tailored for this script, and
+# because of that, it ignored the directory name '.'.
+sub mkdirs {
+ my ($dirs, $mode) = @_;
+ $dirs = &cleandirs($dirs);
+ return if ($dirs eq '.');
+
+ my $newpos = -1;
+ while (($newpos = index($dirs, '/', $newpos+1)) != -1) {
+ my $dir = substr($dirs, 0, $newpos);
+ mkdir ($dir, $mode) || &croak("mkdir $dir failed");
+ }
+ mkdir ($dirs, $mode) || &croak("mkdir $dirs failed");
+}
+
+# Remove a set of directories, failing if the directories
+# contain other files.
+# This subroutine has been tailored for this script, and
+# because of that, it ignored the directory name '.'.
+sub rmdirs {
+ my ($dirs) = @_;
+ $dirs = &cleandirs($dirs);
+ return if ($dirs eq '.');
+
+ rmdir $dirs || &croak("rmdir $dirs failed");
+ my $newpos = length($dirs);
+ while (($newpos = rindex($dirs, '/', $newpos-1)) != -1) {
+ my $dir = substr($dirs, 0, $newpos);
+ rmdir $dir || &croak("rmdir $dir failed");
+ }
+}
+
+# Return a semi-canonical directory name.
+sub cleandirs {
+ my ($dir) = @_;
+ $dir =~ s:/+:/:g;
+ $dir =~ s:/*$::;
+ return $dir;
+}
+
+# Make a temporary directory with mode 0700.
+sub mktmpdir {
+ use File::Temp qw(mkdtemp);
+ my $template = "/tmp/mcuzipfs.XXXXXX";
+ $template="$ENV{MC_TMPDIR}/mcuzipfs.XXXXXX" if ($ENV{MC_TMPDIR});
+ return mkdtemp($template);
+}
+
+# Make a filename absolute and return it.
+sub absolutize {
+ my ($file, $pwd) = @_;
+ return "$pwd/$file" if ($file !~ /^\//);
+ return $file;
+}
+
+# Like the system built-in function, but with error checking.
+# The other argument is an exit status to allow.
+sub safesystem {
+ my ($command, @allowrc) = @_;
+ my ($desc) = ($command =~ /^([^ ]*) */);
+ $desc = File::Basename::basename($desc);
+ system $command;
+ my $rc = $?;
+ &croak("`$desc' failed") if (($rc & 0xFF) != 0);
+ if ($rc != 0) {
+ $rc = $rc >> 8;
+ foreach my $arc (@allowrc) {
+ return if ($rc == $arc);
+ }
+ &croak("`$desc' failed", "non-zero exit status ($rc)");
+ }
+}
+
+# Like backticks built-in, but with error checking.
+sub safeticks {
+ my ($command, @allowrc) = @_;
+ my ($desc) = ($command =~ /^([^ ]*) /);
+ $desc = File::Basename::basename($desc);
+ my $out = `$command`;
+ my $rc = $?;
+ &croak("`$desc' failed") if (($rc & 0xFF) != 0);
+ if ($rc != 0) {
+ $rc = $rc >> 8;
+ foreach my $arc (@allowrc) {
+ return if ($rc == $arc);
+ }
+ &croak("`$desc' failed", "non-zero exit status ($rc)");
+ }
+ return $out;
+}
+
+# Make sure enough arguments are supplied, or die.
+sub checkargs {
+ my $count = shift;
+ my $desc = shift;
+ &croak('missing argument', $desc) if ($count-1 > $#_);
+}
+
+# Quote zip wildcard metacharacters. Unfortunately Info-ZIP zip and unzip
+# on unix interpret some wildcards in filenames, despite the fact that
+# the shell already does this. Thus this function.
+sub zipquotemeta {
+ my ($name) = @_;
+ my $out = '';
+ for (my $c = 0; $c < length $name; $c++) {
+ my $ch = substr($name, $c, 1);
+ $out .= '\\' if (index('*?[]\\', $ch) != -1);
+ $out .= $ch;
+ }
+ return quotemeta($out);
+}