最初にこの方法を見つけた時には目からウロコが落ちました。PNGPNGは非常にシンプルなバイナリの構造をしています。PNGを構成する各セクションはチャンク(Chunk)といい、全てのPNGファイルに含まれるチャンクはRFCによって定められた仕様に沿った規則正しい構造をとるようになっています。(逆に、この仕様に沿わないチャンクやその他の不正なデータを含むファイルはPNGファイルとして解釈されないように画像ビューワやエディタなどの実装は保証されているべきなんですね。)PNGファイル先頭に共通で存在する8バイトのシグネチャを除いて、残りの部分は全て以下のようなチャンクとして分割することができます。4DataLengthには、その後に続くDataの部分のサイズが単位byteで格納されています。NameにはRFCの定義に従って付与されたチャンクの名前、CRC32にはNameとDataのバイナリから計算されたチェックサムの値が格納されています。PNGには多種多様なチャンクが定義されており、ファイルの構成に必須なチャンクの他に補助チャンクやユーザー定義が可能なチャンクなども存在します。実はそれらの中にメタデータとして任意のバイナリやテキストをPNGファイルに含めることができるチャンクが存在するのです。いうわけで、ZIPファイルのバイナリをそのままチャンクのData部として、他のチャンクと同様にDataLengthやName、CRC32などを付与してファイル内に突っ込んであげればZIPファイル入りPNGができることが分かりました。ZIPファイルを隠蔽したあとのPNGファイルは勿論サイズはその分大きくなりますが、隠蔽前と同様に正常に画像として表示することができます。ちなみにPNGの再帰分割処理はわかりやすく簡略化するとこんな感じになってます(そのままだと多分動きません)。詳しい実装についてはリポジトリを見ていただけると助かります。実は今回の実装ではPNGを分割して解析する必要はあまりなかったのですが、ファイルの正規性検査とZIPを含んだチャンクを正しい位置に確実に挿入できるようにするために分割処理を書くことになりました。ZIPこっちは少し難しいです。ZIPもPNGと同様にキレイにデータを分割することができて、Deflateによって圧縮されたデータのバイナリとヘッダのようなものから構成されるようになっています。バイナリの構造はPNGと比べると少し複雑ですが、ちゃんとバックトラッキングすることなくワンパスで解析することができます。ヘッダ内の各データの意味はウィキペディアなどを参照していただけると助かります(量が多すぎてスペースが足りない)。ZIPファイル全体のディスク数(分割数)やオフセット、その他のメタデータなどが格納されている上記のようにヘッダには複数の種類があり、各ヘッダによって構造が違うので、先にヘッダのシグネチャを取り出してからパターンマッチにかけて再帰的に分割...という手法を取らざるを得ませんでした。ヘッダ内のデータのバイナリがリトルエンディアンになっていたりオフセットの値が相対だったり絶対だったり、処理をうまく抽象化することが中々難しく実装したコードのZIP分割処理の部分はかなりuglyな感じになっています。コードは少し汚くても、ZIPファイルはちゃんとキレイにヘッダ+データ毎に分割することができました。次にZIPのバイナリにPNGのバイナリを正しく挿入することになるのですが、実はそのままデータをつなげたり突っ込んだりするだけではエラーが出ます。out.zip適当にこんなことをやっていると、「オフセットが狂ってますよ」みたいな感じのメッセージが吐かれると思います。いうのも、上で示したようにセントラルディレクトリヘッダや終端セントラルディレクトリヘッダにはローカルヘッダとのオフセットやセントラルディレクトリとのオフセットが格納されているため、勝手に先頭に別のデータを追加してしまうと本来それらがあるべき位置がズレてしまうので正しいデータを検出することができなくなってしまうわけですね。ZIPはPNGのようにアーカイブファイルを構成するものの一部として別のデータを含めることができないはずなので、先頭にPNGのデータを追加してオフセットが正しい位置を示すように修正するような方法をとることにします。PNGの場合と同様でほぼ疑似コードのような感じですが、以下のような感じでZIPファイルを解析・分割しながらオフセットをずらすことができます。(もちろんずらす分のオフセットは予め計算して一緒に関数の引数として渡してやる必要があります。<<Ret:32>>.冗長ですが、これ以上抽象化するのは難しそうです...。以上が処理のメインとなるPNGのとZIPのバイナリ解析・分割用の関数についての解説でした。Implementaion関数の実装はOKなので、あとはメインとなる処理を考えるだけです。注意するべきポイントは2つあって、PNGに含めるZIPはチャンクデータとしてそのまま挿入できるが、オフセットをずらすことになるのでCRC32の計算は後回しにする必要がある。ZIPの各ヘッダのオフセットは、PNGのデータ長からIENDのデータ長を差し引いたもの、更にそれに「ZIP用のチャンク」のチャンク名とデータ長を表すデータ分のデータ長を足した値になる。textをDataとして持つチャンク)とし、PNGファイルのIENDチャンクの直前に挿入することにします。そのままではPNGに挿入したZIPのヘッダにあるオフセットがずれてしまうので、先にPNGのIENDチャンクを除いた分にzTXTチャンクのチャンク名とデータ長のデータ分のデータ長をZIPのヘッダのオフセットに足してあげる必要があります。それからZIPのCRC32を計算してPNGファイルに挿入することにします。それらの処理順序などを踏まえた上での生成手順は以下のようになります。PNGをチャンク毎に分割する。ZIPとして格納するためのファイルをZIPする。1の分割済みのPNGのIENDを除いたデータ長を計算し、その値にzTXTのチャンク名とデータ長のデータ分のデータ長を2のZIPのヘッダにあるオフセットに足す。3でできたZIPにzTXTのチャンク名とデータ長とCRC32を計算して付加し、zTXTチャンクを作成する。1で分割したPNGのIENDチャンク直前に4のzTXTチャンクを挿入する。ファイルをPNGファイルとして書き出す。これで一つのファイルにPNGのバイナリとZIPのバイナリが混在した奇妙なファイルができあがります。拡張子を.pngにすると、画像ビューワなどで正常にPNGフォーマットの画像ファイルとして表示することができます。中に含まれるZIPファイルの情報などは表示されません。拡張子を.zipにすると、unzipコマンドなどを用いて正常にZIPアーカイブファイルとして展開することができます。ZIPのバイナリの前に繋がっているPNGファイルは出てきませんし、解凍するときにそれに関する情報なども表示されません。Deflateで圧縮する処理はプログラムの中で行っているので、隠蔽するZIPファイルの中身は自由に決めることができます。PNGファイルやZIPするファイルが大きすぎる場合は検証していないのでそこら辺は自己責任でお願いします。Summaryリポジトリへのリンクや参考資料を以下にリストアップしておきます。今回は完全に趣味の範囲でのプログラムでしたが、これをキッカケにErlangやPNG、ZIPに興味を持ってくれるような方がいればこの珍妙なCLIツールを作った甲斐があったというものです。スターやPRなどお待ちしています。ちなみにツールの悪用は厳禁です。way...正しいけど正しくない?ZIPの分割関数についてなのですが、何故か仕様通りに実装すると動かない部分があって、オフセットを相対の値ではなくて絶対オフセットにしないとちゃんとファイルが解凍されないみたいなんですよね。ウィキペディアやRFCにはオフセットとして相対の値が使われているとの記述がありますが、その通りに実装するとunzipコマンドやWindows標準のアーカイブマネージャーなどではエラーが出て解凍されません。現状オフセットには絶対オフセットを使うようにしており、unzipコマンドでは一切のエラー出力なしで解凍できるのですが、WindowsやmacOSのアーカイブマネージャでは正常に動作しないようです。エラーを見る限りセントラルディレクトリヘッダのオフセットが不正らしいのですが、そうであればunzipコマンドでも同様の警告文が出力されるはずなので謎は深まるばかりです。情報提供お待ちしています。PNGのチャンク名の話zTXTというチャンク名を見てもらえると分かりますが、アルファベット4文字で定義されているチャンク名は不自然に部分的に大文字になったり小文字になったりしています。実はチャンク名は、それぞれの文字が大文字か小文字かでチャンクの持つ特性を表すフラグような役割を果たすようになっているのです。継続使用が可能であることを表すNextさて、明日の記事はsileさんです。バイク月別アーカイブリンクPortfolioはてなブログをはじめよう!mic_psmさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。