第3回の記事ではGo言語の io.Reader
インタフェースの基本と、よく使う構造体を紹介しました。 Go言語が提供する低レベルの仕組みとの接点である io.Writer
と io.Reader
、それに、これらを利用するさまざまな関数や構造体を使って、いろいろな処理ができることが伝わったのではないでしょうか? 第2回から今回までの内容を把握していれば、ソケット通信(第6回あたりから紹介する予定)さえ恐れることはありません。
今回も、前回に引き続き io.Reader
の関連機能を紹介していきます。 具体的には次のような内容です。いずれも少し低レベルな通信を直接行いたいときに便利な機能です。
- バイナリ解析に便利な機能群
- テキスト解析に便利な機能群
- ちょっと抽象的な
io.Reader
の構造体
バイナリ解析用のio.Reader
関連機能
io.Reader
から出てくるデータは、テキストデータの場合もあればバイナリデータの場合もあります。 まずはバイナリデータを読み込むときに便利な機能から紹介します1。
必要な部位を切り出すio.LimitReader
/io.SectionReader
ファイルを読み込みたいけれど、先頭部分だけが必要なので、それ以降の領域は読み込みたくない、という場合があります。 たとえばファイルの先頭にヘッダー領域があって、そこだけを解析するルーチンに処理を渡したいといった場合です。 io.LimitReader
を使うと、データがたくさん入っていても、先頭の一部だけしか読み込めないようにブロックしてくれます。
// たくさんデータがあっても先頭の16バイトしか読み込めないようにする。
lReader := io.LimitReader(reader, 16)
長さだけではなく、スタート位置も固定したいことがあります。 PNGファイルやOpenTypeフォントなど、バイナリファイル内がいくつかのチャンク(データの塊)に分かれている場合は、チャンクごとに Reader
を分けて読み込ませることで、別々のチャンクを読み込むコード間の独立性が高まり、全体としてメンテナンスしやすいコードになるでしょう。 そのようなときに便利なのが io.SectionReader
です。
ただし io.SectionReader
では io.Reader
が使えず、代わりに io.ReaderAt
インタフェースを使います。 os.File
型は io.ReaderAt
を満たしますが、それ以外の io.Reader
を満たす型から io.SectionReader
で直接に読み込むことはできません。 文字列やバイト列にいったん書き出し、 strings.Reader
や bytes.Reader
でラップしてから io.SectionReader
に渡します。
下記のコードでは、文字列から Section
の部分だけを切り出した Reader
をまず作成し、それをすべて os.Stdout
に書き出しています(実際には文字列を分けるために io.SectionReader
を使うことはまずありませんが)。
package main
import (
"io"
"os"
"strings"
)
func main() {
reader := strings.NewReader("Example of io.SectionReader\n")
sectionReader := io.NewSectionReader(reader, 14, 7)
io.Copy(os.Stdout, sectionReader)
}
エンディアン変換
バイナリ解析ではエンディアン変換が必要となります。 現在主流のCPU 2 はリトルエンディアンです(サーバや組み込み機器で使用されるCPUにはビッグエンディアンのものもあります)。 リトルエンディアンでは、10000という数値(16進表示で0x2710)があったときに、小さい桁からメモリに格納されます(Go言語で書けば []byte{0x10, 0x27, 0x0, 0x0}
と表現されます)。
しかし、ネットワーク上で転送されるデータの多くは、大きい桁からメモリに格納されるビッグエンディアン(ネットワークバイトオーダーとも呼ばれます)です。 そのため多くの環境では、ネットワークで受け取ったデータをリトルエンディアンに修正する必要があるのです。
任意のエンディアンの数値を、現在の実行環境のエンディアンの数値に修正するには、encoding/binary
パッケージを使います。 このパッケージの binary.Read()
メソッドに、 io.Reader
とデータのエンディアン、それに変換結果を格納する変数のポインタを渡せば、エンディアンが修正されたデータが得られます。
下記のコードを実行すると、 data
変数にビッグエンディアンで格納されている10000という数値が、エンディアン違いのIntel系のCPUのコンピュータでも正しく10000と出力されます。
package main
import (
"bytes"
"encoding/binary"
"fmt"
)
func main() {
// 32ビットのビッグエンディアンのデータ(10000)
data := []byte{0x0, 0x0, 0x27, 0x10}
var i int32
// エンディアンの変換
binary.Read(bytes.NewReader(data), binary.BigEndian, &i)
fmt.Printf("data: %d\n", i)
}
PNGファイルを分析してみる
PNGファイルはバイナリフォーマットです。 先頭の8バイトがシグニチャ(固定のバイト列)となっています。 それ以降は次のようなチャンク(データの塊)のブロックで構成されています。
長さ | 種類 | データ | CRC(誤り検知符号) |
---|---|---|---|
4バイト | 4バイト | 長さで指定されたバイト数 | 4バイト |
各チャンクとその長さを列挙してみましょう。 以下のコードでは、 readChunks()
関数でチャンクごとに io.SectionReader
を作って配列に格納して返しています。 それをチャンクを表示する関数(dumpChunk()
)で表示しています。
サンプルとして利用しているPNG画像は、コンピュータグラフィックス業界でこれ以上の有名人はいないという、レナ・ソーダバーグさん 3 の画像をお借りしました。
package main
import (
"encoding/binary"
"fmt"
"io"
"os"
)
func dumpChunk(chunk io.Reader) {
var length int32
binary.Read(chunk, binary.BigEndian, &length)
buffer := make([]byte, 4)
chunk.Read(buffer)
fmt.Printf("chunk '%v' (%d bytes)\n", string(buffer), length)
}
func readChunks(file *os.File) []io.Reader {
// チャンクを格納する配列
var chunks []io.Reader
// 最初の8バイトを飛ばす
file.Seek(8, 0)
var offset int64 = 8
for {
var length int32
err := binary.Read(file, binary.BigEndian, &length)
if err == io.EOF {
break
}
chunks = append(chunks, io.NewSectionReader(file, offset, int64(length)+12))
// 次のチャンクの先頭に移動
// 現在位置は長さを読み終わった箇所なので
// チャンク名(4バイト) + データ長 + CRC(4バイト)先に移動
offset, _ = file.Seek(int64(length+8), 1)
}
return chunks
}
func main() {
file, err := os.Open("Lenna.png")
if err != nil {
panic(err)
}
chunks := readChunks(file)
for _, chunk := range chunks {
dumpChunk(chunk)
}
}
実行すると次のように表示されます。
chunk 'IHDR' (13 bytes)
chunk 'sRGB' (1 bytes)
chunk 'IDAT' (473761 bytes)
chunk 'IEND' (0 bytes)
Wikipediaから取得できるレナさんのPNG画像は、このようなチャンクの構図になっているようですね。
Go言語で配列(厳密にはスライス)に要素を追加するには、
配列 = append(配列, 要素)
と書きます。 なお、多くのオブジェクト指向言語と違い、Go言語では配列やスライスはメソッドを持ったオブジェクトではありません (Go言語のメモリ管理についても将来紹介します)。
PNG画像に秘密のテキストを入れてみる
チャンクを追加してみましょう。
PNGには、テキストを追加するための tEXt
というチャンクがあります。また、それに圧縮をかけた zTXt
というチャンクもあります。 このようにPNG画像に埋め込まれたテキストは、データの中に埋め込まれるだけで、画像としては表示されません。
たとえば、先ほどレナさんのPNG画像に、「ASCII PROGRAMMING++」というテキストを tEXt
チャンクを使って追加すると、次の図のような構造のPNG画像になります。
さきほどのコードをコピーして改造し、この秘密のテキスト入りのPNG画像を作ってみましょう。 readChunks()
関数はそのまま残して、 textChunk()
関数を追加し、 main()
関数を置き換えます。 import
の節に "hash/crc32"
を追加します。
func textChunk(text string) io.Reader {
byteData := []byte(text)
var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, int32(len(byteData)))
buffer.WriteString("tEXt")
buffer.Write(byteData)
// CRCを計算して追加
crc := crc32.NewIEEE()
io.WriteString(crc, "tEXt")
binary.Write(&buffer, binary.BigEndian, crc.Sum32())
return &buffer
}
func main() {
file, err := os.Open("Lenna.png")
if err != nil {
panic(err)
}
defer file.Close()
newFile, err := os.Create("Lenna2.png")
if err != nil {
panic(err)
}
defer newFile.Close()
chunks := readChunks(file)
// シグニチャ書き込み
io.WriteString(newFile, "\x89PNG\r\n\x1a\n")
// 先頭に必要なIHDRチャンクを書き込み
io.Copy(newFile, chunks[0])
// テキストチャンクを追加
io.Copy(newFile, textChunk("ASCII PROGRAMMING++"))
// 残りのチャンクを追加
for _, chunk := range chunks[1:] {
io.Copy(newFile, chunk)
}
}
テキストチャンクの中は複雑に見えますが、パーツごとにみれば、それぞれ io.Writer
の書き込みのみで構成されています。 binary.Write()
による長さの書き込み、次にチャンク名の書き込み、本体の書き込み、最後にCRCの計算と、 binary.Write()
による書き込みです。 たったこれだけのコードに5回も io.Writer
による書き込みが登場しています。
main()
関数は、チャンクを io.Copy()
でひたすら書き込んでいます。
読み込むコードも少し改造してみましょう。 tEXt
チャンクの場合にのみ、中身をそのまま表示するようにしておきます。
func dumpChunk(chunk io.Reader) {
var length int32
binary.Read(chunk, binary.BigEndian, &length)
buffer := make([]byte, 4)
chunk.Read(buffer)
fmt.Printf("chunk '%v' (%d bytes)\n", string(buffer), length)
if bytes.Equal(buffer, []byte("tEXt")) {
rawText := make([]byte, length)
chunk.Read(rawText)
fmt.Println(string(rawText))
}
}
さきほど追加したテキストがこんなふうに表示されましたか?
chunk 'IHDR' (13 bytes)
chunk 'tEXt' (19 bytes)
ASCII PROGRAMMING++
chunk 'sRGB' (1 bytes)
chunk 'IDAT' (473761 bytes)
chunk 'IEND' (0 bytes)
注釈
- Go言語はシステムプログラミングに適した言語という触れ込みで連載していますが、残念ながらErlangの文法に組み込まれているバイナリパターンマッチのような強力なバイナリ解析はありません。 ?
- x86、x86_64はリトルエンディアンです。ARMそのものはどちらも切り替えられますが、iOSもAndroidもリトルエンディアンです。 ?
- https://ja.wikipedia.org/wiki/レナ_(画像データ)?
この連載の記事
-
第20回
プログラミング+
Go言語とコンテナ -
第19回
プログラミング+
Go言語のメモリ管理 -
第18回
プログラミング+
Go言語と並列処理(3) -
第17回
プログラミング+
Go言語と並列処理(2) -
第16回
プログラミング+
Go言語と並列処理 -
第15回
プログラミング+
Go言語で知るプロセス(3) -
第14回
プログラミング+
Go言語で知るプロセス(2) -
第13回
プログラミング+
Go言語で知るプロセス(1) -
第12回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(3) -
第11回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(2) -
第10回
プログラミング+
ファイルシステムと、その上のGo言語の関数たち(1) - この連載の一覧へ