こんにちは.
KMC5回生のtyageです.
この記事は, KMCアドベントカレンダー 5日目の記事です.
昨日はnonyleneさんの Android Studio をビルドする でした.
唐突ですが, 今日はPHPの話をします.
TL;DR
PHPのZipArchiveライブラリには, globパターンでファイルを追加する addGlob
メソッドがある.
これにはいくつかoptionが指定できるのだが, 挙動がおかしい.
送った修正PRに反応をもらえたため, 12/1にリリースされた PHP 7.1 に含まれないかなと期待していたが, 特に進展はなかった.
みんな困ってないんか…?
ZipArchive::addGlob
PHPでオシゴトをしていると, PHPでZipファイルを作りたい瞬間があるのではないかと思います.
私も, リクエスト内容が書かれたjsonファイルや画像をZipファイルでまとめてPOSTする超コズミックなAPIを叩く, 最高の機会がありました. (社会の歯車である一労働者は, 用意されたAPIに文句を言う前に黙々と作業をしなければいけない時があります.)
また, 様々な都合でZip圧縮前のフォルダを残しておく必要があったため, こんなコードを書いたのです.
$dir = '/tmp/workdir/'; // create jsons/api.json $jsonDir = $dir . 'jsons'; mkdir($jsonDir); file_put_contents($jsonDir. 'api.json', json_encode($request)); // create archive.zip $zip = new ZipArchive(); $zip->open($dir . 'archive.zip', ZipArchive::CREATE); $zip->addGlob($dir . '**/**', 0, ['remove_path' => $dir]); $zip->close(); |
ZipArchive::addGlob
は, 第一引数にファイル検索パターンを, 第二引数にglobのフラグを, 第三引数にその他optionを指定します.
このoptionが今回の焦点となるのですが, 以下の項目が設定できます.
オプションの連想配列。次のオプションが使えます。
“add_path”
アーカイブ内のファイルのローカルパスに変換するときにつけるプレフィックス。
これが適用されるのは、
“remove_path” や “remove_all_path”
で定義された削除処理がすべて終わった後です。“remove_path”
マッチしたファイルをアーカイブに追加する前に削除するプレフィックス。
“remove_all_path”
TRUE
にすると、ファイル名だけを使ってアーカイブのルートに追加します。
ref: http://php.net/manual/ja/ziparchive.addglob.php
ふむふむ.
今回は remove_path
を指定しており, 期待していた挙動としては,
archive.zip
というファイルができる- そこには
jsons/api.json
というファイルが含まれている
です.
もし仮に, remove_path
を指定していなければ /tmp/workdir/jsons/api.json
というファイルが追加されるため, 正しくAPIを叩けなくなります.
で, 得られた結果がこれです.
/tmp/workdir ᐅ php -v PHP 7.0.13 (cli) (built: Nov 15 2016 23:52:36) ( NTS ) Copyright (c) 1997-2016 The PHP Group Zend Engine v3.0.0, Copyright (c) 1998-2016 Zend Technologies/tmp/workdir ᐅ unzip -l archive.zip Archive: archive.zip Length Date Time Name
6 12-05-2016 18:04 /tmp/workdir/jsons/api.json
6 1 file
あれ? remove_path
optionを指定したのに /tmp/workdir/jsons/api.json
のままですね…
どういうことでしょう?
問題1: add_path optionが必須
調べてみると, どうも add_path
optionが有効でないと他のoptionが機能しないバグがあるようです.
issue や PR はあるものの, まだマージされていませんでした.
仕方ないので, とりあえず add_path
optionをつけて試してみることにします.
$zip->addGlob($dir . '**/**', 0, ['add_path' => 'prefix/', 'remove_path' => $dir]); |
さて, この場合どうなるでしょうか?
/tmp/workdir/jsons/api.json
から /tmp/workdir/
を削って, prefix/
を足すので prefix/jsons/api.json
になりそうですね.
さて, 結果はというと…
/tmp/workdir ᐅ unzip -l archive.zip Archive: archive.zip Length Date Time Name --------- ---------- ----- ---- 6 12-05-2016 18:12 prefix/sons/api.json --------- ------- 6 1 file
oh…
jsons の j がどこかに消えて prefix/sons/api.json
になってしまいました.
問題2: 消えた j の謎
これは霊障でしょうか?
それとも宇宙線の影響でしょうか?
PHPソースコードを見ながら検証してみましょう.
addGlob
でファイルを追加する場合は php_zip_add_from_pattern
という関数を見ればよさそうです. (ext/zip/php_zip.c#L1618)
追加するファイルのファイルパスを操作しているのはここのようです.
remove_path_len
は remove_path
optionの文字数, zval_file
は恐らく検索して見つかったファイルのファイルパスを指しているのでしょう.
1 2 3 4 5 6 7 8 9 10 11 12 | if ((zval_file = zend_hash_index_find(Z_ARRVAL_P(return_value), i)) != NULL) { if (remove_all_path) { basename = php_basename(Z_STRVAL_P(zval_file), Z_STRLEN_P(zval_file), NULL, 0); file_stripped = ZSTR_VAL(basename); file_stripped_len = ZSTR_LEN(basename); } else if (remove_path && strstr(Z_STRVAL_P(zval_file), remove_path) != NULL) { file_stripped = Z_STRVAL_P(zval_file) + remove_path_len + 1; file_stripped_len = Z_STRLEN_P(zval_file) - remove_path_len - 1; } else { file_stripped = Z_STRVAL_P(zval_file); file_stripped_len = Z_STRLEN_P(zval_file); } |
7行目を読んでみると, remove_path
を指定した場合, ファイルパス指すポインタの指すアドレスが remove_path_len + 1
分進みます.
上の例であれば /tmp/workdir/j
の文字数分進みます.
どうやらこれが原因みたいですね.
なんで + 1 されているのかさっぱりわからないのですが, まだバグ報告すらされていないようなので, とりあえずissueを立ててPRを送りましょう.
Bug #72374 ZipArchive::addGlob remove_path option strips first char of filename
Fix #72374: ZipArchive::addGlob remove_path option strips first char of filename #1939
2週間くらいで返事がもらえ, bugfixとして認識してもらえたようです.
ただ, Breaking ChangeなのでPHP7.1.0かPHP8にマージされそうです.(などと言っているうちに7.1.0がリリースされたのですが…)
PRを出してから半年が経過し, このままだと忘れ去られそうなのでここに記録することにしました.
早くマージされるといいなぁ.
既存のテストは?
ところで, remove_path
optionに対するテストがなかったかというと, 実はありました.
ext/zip/tests/oo_addpattern.phpt
こんなコードでテストしています.
$dir = realpath($dirname); $options = array('add_path' => 'baz/', 'remove_path' => $dir); if (!$zip->addPattern('/\.txt$/', $dir, $options)) { echo "failed\n"; } |
ここで, $dirname = "/tmp/workdir/";
であれば, $dir == "/tmp/workdir"
になります.
ということは /tmp/workdir/hoge.txt
から /tmp/workdir
を削って baz/
を先頭に足すoptionになるわけですが, 上記のバグから baz//hoge.txt
ではなく baz/hoge.txt
が生成されます.
おめでたいテストですね.
これも修正対象です.
終わりに
問題の原因は見つかり解決策も出したのですが, まだマージされていません.
自前ビルドのPHPを運用するのも厳しいです.
結局, working directoryを移動して, remove_path
optionを利用しない方向で解決しました.
$dir = '/tmp/workdir/'; chdir($dir); // change working dir! // create jsons/api.json $jsonDir = 'jsons'; mkdir($jsonDir); file_put_contents($jsonDir. 'api.json', json_encode($request)); // create archive.zip $zip = new ZipArchive(); $zip->open('archive.zip', ZipArchive::CREATE); $zip->addGlob('**/**'); $zip->close(); |
もし同じ問題を抱えている人がいれば, この記事が救いになりますように…
明日は dnek_ さんの「1年間の進捗」です.