日本語(シフトJIS)を含んだzipやlzhをUTF-8に変換しながら解凍

シフトJISの日本語ファイル名が入ったzipファイルを受け取ってしまったとき、UTF-8環境のLinux上でunzipすると文字化けして大変なことになります…
解凍後に変換しようとしてもどうしようもないので、SJISからUTF-8にファイル名を変換した上で解凍するPerlスクリプトを勉強がてら作ってみました。
CPANモジュールの Archive::Zip などが必要です。

#!/usr/bin/perl
# ファイル名をSJISからUTF-8に変換しながらzipを解凍する

use strict;
use warnings;
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Encode qw/from_to/;
use Getopt::Long;
use DateTime;
use DateTime::TimeZone::Local;

GetOptions('list|l' => \ my $mode_list);
if (scalar @ARGV < 1) {
  print "Usage: $0 [-l] <zip file>\n";
  exit 1;
}
my $zipfile = shift @ARGV;

my $zip = Archive::Zip->new();
unless ( $zip->read($zipfile) == AZ_OK ) {
  die 'read error';
}
print "Archive: $zipfile\n";
if ($mode_list) {
  print <<HERE;
   Length     Date    Time   Name
 --------  ---------- -----  ----
HERE
}
my $total_size = 0;
my @members = $zip->members();
my $members_count = scalar @members;
foreach my $member (@members) {
  my $filename = $member->fileName;
  from_to($filename, 'cp932', 'utf8');
  if ($mode_list) { # リスト表示モード
    my $dt = DateTime->from_epoch(
      epoch => $member->lastModTime
    )->set_time_zone(DateTime::TimeZone::Local->TimeZone());
    $total_size += $member->uncompressedSize;
    printf "%9d  %s  %s\n",
      $member->uncompressedSize,
      $dt->ymd('-') . ' ' . sprintf("%02d", $dt->hour) . ':' . sprintf("%02d", $dt->min),
      $filename;
  } else { # 解凍モード
    print "  inflating: $filename\n";
    unless (defined $zip->extractMember($member, $filename)) {
      warn "  failed: $filename\n";
    }
  }
}
my $members_count_str = "$members_count file" . ($members_count > 1 ? 's' : '');
if ($mode_list) {
  print <<HERE;
 --------                    -------
HERE
printf "%9d                    %s\n", $total_size, $members_count_str;
}

これを /usr/local/bin/unzips とかに保存して下のコマンドで展開できます。
ファイルの上書き確認なんていう上等な機能はまだないです。

$ unzips <zip file>

-l オプションで中身の一覧表示ができます。

$ unzips -l <zip file>


ついでに日本語ファイル名を含むlzhファイルを送ってこられる方々が私の周りに少なからずいるのでそのとき用に下のスクリプトも作りました。
CPANモジュールの Archive::Lha などが必要です。

#!/usr/bin/perl
# ファイル名をSJISからUTF-8に変換しながらlzhを解凍する

use strict;
use warnings;
use Archive::Lha::Stream;
use Archive::Lha::Header;
use Archive::Lha::Decode;
use Encode qw/from_to/;
use File::Basename;
use File::Path;
use Getopt::Long;
use DateTime;
use DateTime::TimeZone::Local;

GetOptions('list|l' => \ my $mode_list);
if (scalar @ARGV < 1) {
  print "Usage: $0 [-l] <lzh file>\n";
  exit 1;
}
my $lzhfile = shift @ARGV;

my $stream = Archive::Lha::Stream->new(file => $lzhfile);
print "Archive: $lzhfile\n";
if ($mode_list) {
  print <<HERE;
   Length     Date    Time   Name
 --------  ---------- -----  ----
HERE
}
my $total_size = 0;
my $members_count = 0;
while (defined(my $level = $stream->search_header)) {
  $members_count++;
  my $header = Archive::Lha::Header->new(
    level  => $level,
    stream => $stream,
  );
  my $filename = $header->pathname;
  $filename =~ s{\\}{/}g;
  $filename =~ s/\xff//g;
  from_to($filename, 'cp932', 'utf8');
  if ($mode_list) { # リスト表示モード
    my $dt = DateTime->from_epoch(
      epoch => $header->timestamp
    )->set_time_zone(DateTime::TimeZone::Local->TimeZone());
    $total_size += $header->original_size;
    printf "%9d  %s  %s\n",
      $header->original_size,
      $dt->ymd('-') . ' ' . sprintf("%02d", $dt->hour) . ':' . sprintf("%02d", $dt->min),
      $filename;
    $stream->seek( $header->next_header );
  } else { # 解凍モード
    print "  inflating: $filename\n";
    my $dir = dirname($filename);
    mkpath($dir) unless -e $dir;
    open my $fh, '>:raw', $filename;
    binmode $fh;
    $stream->seek($header->data_top);
    my $decoder = Archive::Lha::Decode->new(
      header => $header,
      read   => sub { $stream->read(@_) },
      write  => sub { print $fh @_ },
    );
    my $crc16 = $decoder->decode;
    close $fh;
    warn "crc mismatch" if $crc16 != $header->crc16;
  }
}
my $members_count_str = "$members_count file" . ($members_count > 1 ? 's' : '');
if ($mode_list) {
  print <<HERE;
 --------                    -------
HERE
printf "%9d                    %s\n", $total_size, $members_count_str;
}

これも /usr/local/bin/unlzhs とかに保存して下のコマンドで実行できます。
くれぐれも、上書き確認機能はないですので使う際はご注意ください。
機能的には lha x のようなものです。

$ unlzhs <lzh file>

-l オプションで中身を一覧表示できます。
機能的には lha l のようなものです。

$ unlzhs -l <lzh file>