emahiro/b.log

勉強記録と書評とたまに長めの呟きを書きます

【学習】「GOならわかるシステムプログラミング」~Chapter 3~

※「GOならわかるシステムプログラミング」の3章の学習記録です。

主に

  • PNGファイルを分析してみる
  • PNGファイルに秘密のテキストを入れてみる

の2節の内容の学習記録です。

sample

sampleコードは以下

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "hash/crc32"
    "io"
    "os"
)

const (
    OFFSET = 8
)

func dumpChunk(chunk io.Reader) {
    var l int32
    binary.Read(chunk, binary.BigEndian, &l)
    // png をbyteに変換した時の入れ物
    buf := make([]byte, 4)
    chunk.Read(buf)
    fmt.Printf("chunk '%v' (%d byte)\n", string(buf), l)
    if bytes.Equal(buf, []byte("tExt")) {
        rawText := make([]byte, l)
        chunk.Read(rawText)
        fmt.Printf("%s\n", string(rawText))
    }
}

func readChunks(file *os.File) []io.Reader {
    // pngをchunkに分けたときのchunkを格納する入れ物
    var chunks []io.Reader

    // pngの最初の8byteを飛ばして9byte目から読み込む
    file.Seek(OFFSET, 0)
    var ofs int64 = OFFSET

    for {
        // data長のいれもの
        var l int32
        err := binary.Read(file, binary.BigEndian, &l)
        if err == io.EOF {
            break
        }

        chunks = append(chunks, io.NewSectionReader(file, ofs, int64(l)+12))

        // 次のchunkの先頭に移動
        ofs, _ = file.Seek(int64(l+8), 1)
    }

    return chunks
}

func textChunk(txt string) io.Reader {
    byteData := []byte(txt)
    var buf bytes.Buffer
    binary.Write(&buf, binary.BigEndian, int32(len(byteData))) // 書き込むためのbufferを確保。長さは入力文字長。
    buf.WriteString("tExt")                                    //bufferに書き込む
    buf.Write(byteData)                                        // 入力対象の文字をbufferに書き込む
    // CRCを計算して追加
    crc := crc32.NewIEEE()
    io.WriteString(crc, "tExt")
    binary.Write(&buf, binary.BigEndian, crc.Sum32()) // crc分を新しくbufferに書き込む
    return &buf                                       // 書き込み終わったbufferを返す
}

func WriteNewTextChunk() {
    // pngをチャンクに分けて読み込み
    file, err := os.Open("./imgs/Lenna.png")
    if err != nil {
        fmt.Printf("error! err: %v", err)
        os.Exit(-1)
    }
    defer file.Close()

    newFile, _ := os.Create("./imgs/result.png")
    defer newFile.Close()

    chunks := readChunks(file)
    // pngのシグニチャー書き込み
    io.WriteString(newFile, "\x89PNG\r\n\x1a\n")

    // 先頭にIHDR chunkを書き込む
    io.Copy(newFile, chunks[0])

    // TextChunk を追加する
    io.Copy(newFile, textChunk("Test Text Chunk"))

    // 残りのチャンクを新しく追加する
    for _, c := range chunks[1:] {
        io.Copy(newFile, c)
    }
}

func main() {
    // 私いchunkを書き込んだfileを生成する
    WriteNewTextChunk()

    file, err := os.Open("./imgs/result.png")
    if err != nil {
        fmt.Printf("error! err: %v", err)
        os.Exit(-1)
    }

    chunks := readChunks(file)
    for _, c := range chunks {
        dumpChunk(c)
    }
}

githubはこちら

github.com

TOPIC

エンディアン変換について

エンディアン変換(以下 Byte Swapping)がgoには標準で装備されています。
CPUはデータの先頭には小さい桁から格納されるリトルエンディアンが採用されていますが、ネットワーク(TCP/IPプロトコル)ではデータの先頭に大きな桁から格納されるビックエンディアンが採用されています。

そのため、ビックエンディアンで格納されているネットワークで受け取ったデータをリトルエンディアンに修正する必要があります。

それが何度か出てくる以下の部分

binary.Read(r io.Reader, binary.BigEndian, data interface{})

TCP/IPプロトコルではデータは BigEndian なので、rは BigEndian で格納されているので、binary.Read するときにorderに BigEndian を指定することで、そのorderでio.Readerからデータを読み込み、dataの中に格納し直します。格納するときはリトルエンディアンに修正して格納されます。

binaryパッケージ

encoding/binary についてのメモです。

binary.Read

encoding/binary パッケージ - golang.jp

Read関数

func Read(r io.Reader, order ByteOrder, data interface{}) os.Error Readは、構造化されたバイナリデータをrから読み込みます。データは固定サイズの値へのポインタ、または固定サイズの値のスライスでなければいけません。

バイナリかされたio.Reader型のデータから、任意のdata(interface型)へ変換する

binary.Write

encoding/binary パッケージ - golang.jp

func Write(w io.Writer, order ByteOrder, data interface{}) os.Error Writeは、wへdataのバイナリ表現を書き込みます。データは固定サイズの値、または固定サイズの値へのポインタでなければいけません。

data := "Hello World"
var buf bytes.Buffer
binary.Write(&buf, binary.BigEndian, int32(len(data))) // dataサイズの長さをbufに書き込む
buf.Write([]byte(data)) // 確保したbufferにdataの内容を書き込む

binary表現の書き込みがどう行われているのか、知る機械になってよかった。こういったことはシステムプログラミングをしながらわかることなので、引き続き読み進めようと思います。