ZipArchiveで日本語ファイル名を扱えない場合があって困った話

More than 5 years have passed since last update.

PHPのZipArchiveは実行時のlocaleに依存するため、マルチバイト文字がファイル名に含まれているzipファイルを展開しようとした場合に、localeがCだったりすると、日本語を正しく扱えず、あいうえお.txtのようなファイルが.txtのような名前で展開されてしまったりする。

これはZipArchiveに限らず、basenameなど多くの関数で影響を受けるので、例えばpathinfo関数には注意書きがあったりする。

ZipArchiveにはその注意書きがなく、localeに依存しているとは気付かずはまったので、localeに依存しないextractToを書いてみました。

やっつけなので、おかしな動きをするかもしれないです。


zip.php

<?php

function extractTo($zip_src, $dest_dir, $to_encoding = 'UTF-8')
{
$zip = new \ZipArchive;
if ($zip->open($zip_src) !== true) {
return false;
}

$hex = file_get_contents($zip_src, false, null, 7, 1);
$from_encoding = (ord($hex) & 0x08) === 0x08 ? 'UTF-8' : 'CP932';
$same_encoding = $from_encoding === $to_encoding;

$num_files = $zip->numFiles;
for ($i = 0; $i < $num_files; $i++) {
$stat = $zip->statIndex($i);
$name = $same_encoding
? $stat['name']
: mb_convert_encoding($stat['name'], $to_encoding, $from_encoding);

if ($name[strlen($name)-1] === '/') {
continue;
}

$splited = explode('/', $name);
$basename = array_pop($splited);

$dir = empty($splited)
? $dest_dir.implode('/', $splited).'/'
: $dest_dir;

if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}

file_put_contents("{$dir}{$basename}", $zip->getFromIndex($i), LOCK_EX);
}

return $zip->close();
}


zipの仕様を調べたわけではないので、$stat['name']に期待している形式の名前が入っていなかった場合にはおかしなことになる気がします。


参考

PHPのロケールに関するまとめ

http://d.hatena.ne.jp/hnw/20120501


追記

コード中でmb_strlenを使っていましたが、strlenを使うのが正しいです。


さらに追記

ファイル名にはCP932またはUTF-8が含まれるらしいことがわかったので、エンコーディングを変更できるように拡張しました。

file_get_contentsのところで失敗する場合もあるのでもう少しチェックをしっかりしたほうが良さそうですね。