Golangでバイナリ等のbit操作を行うためのライブラリ go-bit
加筆修正したものをこちらに書きました。v2.2.0からbinary.Write相当のAPI、bit.Writeをサポートしました。
Golangでバイナリ等のbit操作を行うためのライブラリ go-bit | Zenn
概要
go で 主にバイナリファイルの読み出し等に使えるbit操作ライブラリを作りました。*1
binary.Readのように、構造体にbit配列を定義して読みだすことができます。
目次
特徴
下記のように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と非互換です。