みなさんはバイナリデータの中身を覗いたことがありますか。
僕は普段、WEBアプリケーションの開発に携わっているのですが、どうしてもこういった領域とは接点がありません。一度、WireSharkというアプリケーションを使って自身のPCから外部に送信されているパケット(バイナリデータ)の中身を覗いたことがあるぐらいです。
先日、システムコールを使ってTCPサーバーを実装したように、最近は触れたことのない領域にトライしています。
今回はPNGファイルを読み込んで、中身を覗いてみたいと思います。
具体的にはPNGファイルを指定バイトずつ読み込んで、どんな値が指定されているのかを確認していきます。
PNGファイルを選択したのはフォーマットが比較的シンプルで、たまたまデスクトップに.png
形式のデータが転がっていたからです。
PNGファイルのフォーマットについて
まずはPNGファイルのフォーマットについて知ることから始めます。
ここでいうフォーマットとは先頭からNバイト分はAについての情報、次のYバイト分はBについての情報...といったようにISOなどの国政標準化機構によって定められたものです。
PNGはPortable Network Graphics
の略称であり、やはり国際基準に従っています。
Portable Network Graphics - Wikipedia
PNGファイルは次の順序で値が記録されています。
フォーマットについてはこちらのサイトを参考にさせて頂きました。
- PNGファイルシグネチャ: 8バイト
- IHDRチャンク(イメージヘッダー): 25バイト
- 補助チャンク(必須ではない)
- IDATチャンク(イメージデータ本体): 可変長
- IENDチャンク(終端を示す): 12バイト
つまり、先頭から8バイト分だけ読み込めばPNGファイルのシグネチャを値が取得できます。
同様に次の25バイト分を読み込めばIHDRチャンク(イメージヘッダー)の値も取得できそうです。
読み込んだIHDRチャンク25バイト分のバイナリーには画像の幅・高さといった値がそれぞれ指定されています。
ただし、今回は補助チャンクは扱いません。
理由としては、補助チャンクまで対応するとデータの用意が面倒ですし、処理が複雑になるためです。
用意したデータ
適当に自身のプロフィール画像にしました。
詳細情報から確認できた情報は以下になります。
- サイズ: 2090バイト
- 大きさ: 64x64
- 色空間: Gray
- アルファチャンネル: はい
いざ覗いてみる
事前知識も得られたので、いざPNGファイルの中身を覗いていきます。
前回、音声ファイルをTCPサーバーから配信した際にNバイトずつ読み込んだのと全く同じ方法でいけました。
先ほどのフォーマットに従い、指定バイト分ずつバイナリーを読み込んでいきます。
func ReadBinary(file *os.File, x int) ([]byte, error) { data := make([]byte, x) if _, err := file.Read(data); err != nil { return nil, err } return data, nil }
ただし、IHDRチャンクに関しては可変長のため、どれだけバイナリを読み込めば良いのか分かりません。
そのためファイルサイズから先頭のPNGファイルシグネチャ(8バイト)とIHDRチャンク(25バイト)、終端のIENDチャンク(12バイト)の差分の分だけ読み込むようにしました。
const ( HEADER_SIZE = 8 IHDR_SIZE = 25 IEND_SIZE = 12 ) png, err := os.Open("./profile.png") : fileInfo, err := png.Stat() : fileSize := fileInfo.Size() offset := fileSize - (HEADER_SIZE + IHDR_SIZE + IEND_SIZE) idat, err := ReadBinary(png, int(offset))
断片的にコードを紹介しましたが、全体はGithubにて公開しています。
最終的に読み込んだバイナリーは特に使う予定はありませんが、構造体に変換しています。
読み込んだバイナリーの検証
PNGファイルをそれぞれのチャンクに分割して読み込んでみた所、以下のようになりました。
IDATチャンクの本体データに関しては文字数の都合上、省略しています。
--Signature--------- 0x89,0x50,0x4e,0x47,0xd,0xa,0x1a,0xa --Ihdr--------- Length: 0x0,0x0,0x0,0xd, ChunkType: 0x49,0x48,0x44,0x52, ChunkWidth: 0x0,0x0,0x0,0x40, ChunkHeight: 0x0,0x0,0x0,0x40, BitDepth: 0x8 ColorType: 0x4 BitDepth: 0x8 Compression: 0x0 Filter: 0x0 Interrace: 0x0 Crc: 0x0,0x60,0xb9,0x55 --Idat--------- Length: 0x0,0x0,0x7,0xf1, ChunkType: 0x49,0x44,0x41,0x54, ChunkData: [xxxxxxxxx] Crc: 0xac,0x17,0xa7,0x92 --Iend--------- Length: 0x0,0x0,0x0,0x0, ChunkType: 0x49,0x45,0x4e,0x44, Crc: 0xae,0x42,0x60,0x82
まずsignature
がPNGであることを示す89 50 4E 47 0D 0A 1A 0A
と一致していることが確認できます。
残りのチャンクについては期待値が明確なものだけを選抜して確認しました。
チャンク | 名称 | 結果 | 期待値 |
---|---|---|---|
IHDR | Length | 0x0,0x0,0x0,0xd(13) | 13(0xd) |
ChunkType | 0x49,0x48,0x44,0x52 | 49 48 44 52 | |
Width | 0x0,0x0,0x0,0x40(65) | 64 | |
Height | 0x0,0x0,0x0,0x40(65) | 64 | |
ColorType | 0x4 | 4(αチャンネル) | |
IDAT | ChunkType | 0x49,0x44,0x41,0x54 | 49 44 41 54 |
IEND | ChunkType | 0x49,0x45,0x4e,0x44 | 49 45 4E 44 |
画像サイズが1だけ異なるのが気になりますが、概ね期待通り、バイナリーを読み取ることができたようです。
今回は補助チャンクの対応を行っていないので、単に上から指定バイト分だけ読み取る操作を繰り返すだけでしたが、実際には補助チャンクのAがあるとBが...CがあるとDが...と処理は複雑になると思います。
最後に
過去にプロトコルを自作する書籍に取り組んだことがありますが、よく分かりませんでした。
先頭から4バイトにはプロトコルの種類を記録して...とバイトで値を表現するという知識が当時の自分にはありませんでした。
今回は簡単なPNGファイルを覗いて、実際にバイナリーで表現された値を確認することができました。
どのように設計するかは非常に難しそうですが、バイナリーさえ作成してしまえばTCPなりで送受して解析すれば良いだけなので、やれることは多そうです。
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。