uzip.in 14 KB

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