💱

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

2020/10/04に公開

自ブログ から加筆修正して転載したものです。

概要

go で 主にバイナリファイルの読み出し等に使えるbit操作ライブラリを作りました。((エンディアンの理解が怪しかったので結構苦労しました。。))

binary.Readのように、byte slice からbit列(やそれを含む構造体)を読みだして埋めてくれます。
また、v2.2.0からbinary.Writeのように、bit列(やそれを含む構造体)をbyte slice に埋めることもできるようになりました。

https://github.com/nokute78/go-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に投げれば、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を参照しました。
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と非互換です。

Discussion