Golangでバイナリ等のbit操作を行うためのライブラリ go-bit
自ブログ から加筆修正して転載したものです。
概要
go で 主にバイナリファイルの読み出し等に使えるbit操作ライブラリを作りました。((エンディアンの理解が怪しかったので結構苦労しました。。))
binary.Readのように、byte slice からbit列(やそれを含む構造体)を読みだして埋めてくれます。
また、v2.2.0からbinary.Writeのように、bit列(やそれを含む構造体)をbyte slice に埋めることもできるようになりました。
特徴
下記のように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に投げれば、byte列をいい感じに解釈してその構造体に埋めて返してくれる機能があります。
つまり、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のデータ構造を持っているので、それを読んでみましょう。 ((あくまで参考例として。golangには "archive/zip" にZipのFileHeaderが定義されているので、通常はこういう定義はしないでしょうけれど。))
ファイルヘッダについては4.3.7 、general purpose bit flagについては、下記の4.4.4を参照しました。
下記はサンプルコードです。
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で読むということができないようです。
数値型のbyteサイズでエンディアンが判定される様子。
bit.Readではbyte arrayを渡せばエンディアンを反映して埋めてくれるようにしてあり、そこはbinary.Readと非互換です。
Discussion