Golangでバイナリ等のbit操作を行うためのライブラリ go-bit

加筆修正したものをこちらに書きました。v2.2.0からbinary.Write相当のAPI、bit.Writeをサポートしました。

Golangでバイナリ等のbit操作を行うためのライブラリ go-bit | Zenn

概要

go で 主にバイナリファイルの読み出し等に使えるbit操作ライブラリを作りました。*1

binary.Readのように、構造体にbit配列を定義して読みだすことができます。

github.com

目次

特徴

下記のようにbitを定義でき、bit.Readを使用することでいい感じに埋めて返してくれます。

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "github.com/nokute78/go-bit/pkg/bit/v2"
    "io"
)

type BitMap struct {
    Bit0     bit.Bit
    Bit1     bit.Bit
    Bit2     bit.Bit
    Bit3     bit.Bit
    Reserved [4]bit.Bit `bit:"skip"`
}

func ReadBitMap(r io.Reader, b binary.ByteOrder) (*BitMap, error) {
    ret := &BitMap{}
    if err := bit.Read(r, b, ret); err != nil {
        return nil, err
    }
    return ret, nil
}

func main() {
    buf := bytes.NewBuffer([]byte{0xf5})
    bm, err := ReadBitMap(buf, binary.LittleEndian)
    fmt.Printf("bm=%+v err=%s\n", bm, err)

}

作った背景

レジスタやバイナリファイルの仕様を見ていると、bitごとに意味が割り当てられていて、これらの意味を解釈するためには、byte配列等で読みだした上で&や|の演算子を駆使し、bit演算をする必要があります。

また、golangにはmath/bits というbit操作用のパッケージがあるのですが、これは主に演算用のもので、上記のようなbitの取り出しには不向きなようでした。(四則演算やその際にキャリーするとか、ビットの並べ替えをするとか、そういう用途のようです。)

また、goにはencoding/binaryという便利なパッケージがあり、定義済の構造体をbinary.Readに投げれば、バイナリファイルをいい感じに解釈してその構造体に埋めて返してくれる機能があります。

まあ、binary.Readのbit版が欲しかったのです。構造体にbitの定義をして、投げればいい感じに埋めて返してくれるAPIが。

インストール

下記のようにv2をgo get してください。(トップディレクトリのものは古いです)

go get github.com/nokute78/go-bit/pkg/bit/v2

StuctTag

いくつかのStruct Tagをサポートしています。

タグ 内容
`bit:"skip"` このフィールドは無視します。オフセットはフィールドサイズ分だけ移動します。Reservedな値に対して使うと良いです。
`bit:"-"` このフィールドは無視します。オフセットは移動しません。
`bit:"BE"` このフィールドはBigEndianとして扱う。Mixed Endianなデータの解析に便利。(ただしbit向きでなく、後述のbyte array用)
`bit:"LE"` このフィールドはLittleEndianとして扱う。Mixed Endianなデータの解析に便利。(ただしbit向きでなく、後述のbyte array用)

サンプルコード

zipファイルにはヘッダにgeneral purpose bit flagという16bitのデータ構造を持っているので、それを読んでみましょう。 *2

ファイルヘッダについては4.3.7 、general purpose bit flagについては、下記の4.4.4を参照しました。 https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT

下記はサンプルコードです。

zip file header を構造体として定義し、それをbit.Readに渡しているのがポイントです。

package main

import (
    "encoding/binary"
    "flag"
    "fmt"
    "github.com/nokute78/go-bit/pkg/bit/v2"
    "os"
)

type ZipHeader struct {
    Signature  uint32
    MinVersoin uint16
    // general purpose bit flag: 2bytes
    Encrypted           bit.Bit
    CompMode            [2]bit.Bit
    Crc32CompUnCompUsed bit.Bit
    ReservedForMethod8  bit.Bit `bit:"skip"`
    CompPatchedData     bit.Bit
    StrongEncryption    bit.Bit
    Unused              [4]bit.Bit `bit:"skip"`
    LanguageEncoding    bit.Bit
    Reserved            bit.Bit
    HideLocalHeader     bit.Bit
    Reserved2           [2]bit.Bit `bit:"skip"`
    CompMethod          uint16
    LastModTime         uint16
    LastModData         uint16
    Crc32               uint32
    CompSize            uint32
    UnCompSize          uint32
    FileNameLen         uint16
    ExtraFieldLen       uint16
}

func main() {
    flag.Parse()

    var zh ZipHeader
    for _, v := range flag.Args() {
        f, err := os.Open(v)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Open(%s): err=%s\n", v, err)
            continue
        }

        err = bit.Read(f, binary.LittleEndian, &zh)
        if err != nil {
            fmt.Fprintf(os.Stderr, "Read(%s): err=%s\n", v, err)
            f.Close()
            continue
        }
        fmt.Printf("header(%s):%+v\n", v, zh)
        f.Close()
    }

}

go のインストールディレクトリにはテスト用のzipファイルがいくつか有ったので、試しに読んでみました。

$ go run zipexample.go /usr/local/go/src/archive/zip/testdata/dd.zip 
header(/usr/local/go/src/archive/zip/testdata/dd.zip):{Signature:67324752 MinVersoin:20 Encrypted:0 CompMode:[0 0] Crc32CompUnCompUsed:1 ReservedForMethod8:0 CompPatchedData:0 StrongEncryption:0 Unused:[0 0 0 0] LanguageEncoding:0 Reserved:0 HideLocalHeader:0 Reserved2:[0 0] CompMethod:8 LastModTime:26826 LastModData:15938 Crc32:0 CompSize:0 UnCompSize:0 FileNameLen:8 ExtraFieldLen:0}

Bit3(Crc32CompUnCompUsed)が立っていますね。これが立っているとcrc-32/compressed size/uncompressed size が0になるそうです。それらがキチンと0になっている様子が見られます。

下記のようにBit3が落ちている場合の例も貼っておきます。これらはcrc-32や各サイズが非0ですね。

$ go run zipexample.go /usr/local/go/src/archive/zip/testdata/unix.zip 
header(/usr/local/go/src/archive/zip/testdata/unix.zip):{Signature:67324752 MinVersoin:10 Encrypted:0 CompMode:[0 0] Crc32CompUnCompUsed:0 ReservedForMethod8:0 CompPatchedData:0 StrongEncryption:0 Unused:[0 0 0 0] LanguageEncoding:0 Reserved:0 HideLocalHeader:0 Reserved2:[0 0] CompMethod:0 LastModTime:20620 LastModData:16264 Crc32:2098461837 CompSize:8 UnCompSize:8 FileNameLen:5 ExtraFieldLen:28}

なお、README.mdではBig Endianでも動作するか確認するため、一例としてTCPHeaderのパースを行っています。

そのほか

golangのbinary.Readでは、例えば6byteをBig Endianで読むということができないようです。

https://play.golang.org/p/RUvRdkc0a0W

https://github.com/golang/go/issues/40891

数値型のbyteサイズでエンディアンが判定される様子。

bit.Readではbyte arrayを渡せばエンディアンを反映して埋めてくれるようにしてあり、そこはbinary.Readと非互換です。

*1:エンディアンの理解が怪しかったので結構苦労しました。。

*2:あくまで参考例として。golangには "archive/zip" にZipのFileHeaderが定義されているので、通常はこういう定義はしないでしょうけれど。