diff options
-rw-r--r-- | cmd/btrfs-dbg/main.go | 105 | ||||
-rw-r--r-- | cmd/btrfs-dbg/types.go | 80 | ||||
-rw-r--r-- | go.mod | 5 | ||||
-rw-r--r-- | go.sum | 11 | ||||
-rw-r--r-- | pkg/binstruct/binstruct_test.go | 49 | ||||
-rw-r--r-- | pkg/binstruct/l1.go | 163 | ||||
-rw-r--r-- | pkg/binstruct/l2.go | 156 | ||||
-rw-r--r-- | pkg/binstruct/l3.go | 61 |
8 files changed, 630 insertions, 0 deletions
diff --git a/cmd/btrfs-dbg/main.go b/cmd/btrfs-dbg/main.go new file mode 100644 index 0000000..8758cd0 --- /dev/null +++ b/cmd/btrfs-dbg/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "os" + + "lukeshu.com/btrfs-tools/pkg/binstruct" +) + +func main() { + if err := Main(os.Args[1]); err != nil { + fmt.Fprintf(os.Stderr, "%s: error: %v\n", os.Args[0], err) + os.Exit(1) + } +} + +func Main(imgfilename string) (err error) { + maybeSetErr := func(_err error) { + if _err != nil && err == nil { + err = _err + } + } + + fh, err := os.Open(imgfilename) + if err != nil { + return err + } + img := &Img{ + File: fh, + } + defer func() { + maybeSetErr(img.Close()) + }() + + superblocks, err := img.Superblocks() + if err != nil { + return err + } + fmt.Printf("%#v\n", superblocks[0]) + + return nil +} + +type Img struct { + *os.File +} + +func (img *Img) Size() (int64, error) { + fi, err := img.Stat() + if err != nil { + return 0, err + } + return fi.Size(), nil +} + +type Ref[T any] struct { + img *Img + addr int64 + data T +} + +func (r *Ref[T]) Read() error { + size, err := binstruct.Size(r.data) + if err != nil { + return err + } + buf := make([]byte, size) + if _, err := r.img.ReadAt(buf, r.addr); err != nil { + return err + } + return binstruct.Unmarshal(buf, &r.data) +} + +func (img *Img) Superblocks() ([]Ref[Superblock], error) { + const superblockSize = 0x1000 + + var superblockAddrs = []int64{ + 0x00_0001_0000, // 64KiB + 0x00_0400_0000, // 64MiB + 0x40_0000_0000, // 256GiB + } + + sz, err := img.Size() + if err != nil { + return nil, err + } + + var ret []Ref[Superblock] + for i, addr := range superblockAddrs { + if addr+superblockSize <= sz { + superblock := Ref[Superblock]{ + img: img, + addr: addr, + } + if err := superblock.Read(); err != nil { + return nil, fmt.Errorf("superblock %d: %w", i, err) + } + ret = append(ret, superblock) + } + } + if len(ret) == 0 { + return nil, fmt.Errorf("no superblocks") + } + return ret, nil +} diff --git a/cmd/btrfs-dbg/types.go b/cmd/btrfs-dbg/types.go new file mode 100644 index 0000000..33f78ba --- /dev/null +++ b/cmd/btrfs-dbg/types.go @@ -0,0 +1,80 @@ +package main + +import ( + "lukeshu.com/btrfs-tools/pkg/binstruct" +) + +type ( + PhysicalAddr int64 + LogicalAddr int64 + UUID [16]byte +) + +type Superblock struct { + Checksum [0x20]byte `bin:"off=0, siz=20, desc=Checksum of everything past this field (from 20 to 1000)"` + FSUUID UUID `bin:"off=20, siz=10, desc=FS UUID"` + Self PhysicalAddr `bin:"off=30, siz=8, desc=physical address of this block (different for mirrors)"` + Flags uint64 `bin:"off=38, siz=8, desc=flags"` + Magic [8]byte `bin:"off=40, siz=8, desc=magic ('_BHRfS_M')"` + Generation uint64 `bin:"off=48, siz=8, desc=generation"` + + RootTree LogicalAddr `bin:"off=50, siz=8, desc=logical address of the root tree root"` + ChunkTree LogicalAddr `bin:"off=58, siz=8, desc=logical address of the chunk tree root"` + LogTree LogicalAddr `bin:"off=60, siz=8, desc=logical address of the log tree root"` + + LogRootTransID uint64 `bin:"off=68, siz=8, desc=log_root_transid"` + TotalBytes uint64 `bin:"off=70, siz=8, desc=total_bytes"` + BytesUsed uint64 `bin:"off=78, siz=8, desc=bytes_used"` + RootDirObjectID uint64 `bin:"off=80, siz=8, desc=root_dir_objectid (usually 6)"` + NumDevices uint64 `bin:"off=88, siz=8, desc=num_devices"` + + SectorSize uint32 `bin:"off=90, siz=4, desc=sectorsize"` + NodeSize uint32 `bin:"off=94, siz=4, desc=nodesize"` + LeafSize uint32 `bin:"off=98, siz=4, desc=leafsize"` + StripeSize uint32 `bin:"off=9c, siz=4, desc=stripesize"` + SysChunkArraySize uint32 `bin:"off=a0, siz=4, desc=sys_chunk_array_size"` + + ChunkRootGeneration uint64 `bin:"off=a4, siz=8, desc=chunk_root_generation"` + CompatFlags uint64 `bin:"off=ac, siz=8, desc=compat_flags"` + CompatROFlags uint64 `bin:"off=b4, siz=8, desc=compat_ro_flags - only implementations that support the flags can write to the filesystem"` + IncompatFlags uint64 `bin:"off=bc, siz=8, desc=incompat_flags - only implementations that support the flags can use the filesystem"` + ChecksumType uint16 `bin:"off=c4, siz=2, desc=csum_type - Btrfs currently uses the CRC32c little-endian hash function with seed -1."` + + RootLevel uint8 `bin:"off=c6, siz=1, desc=root_level"` + ChunkLevel uint8 `bin:"off=c7, siz=1, desc=chunk_root_level"` + LogLevel uint8 `bin:"off=c8, siz=1, desc=log_root_level"` + + DevItem DevItem `bin:"off=c9, siz=62, desc=DEV_ITEM data for this device"` + Label [0x100]byte `bin:"off=12b, siz=100, desc=label (may not contain '/' or '\\')"` + CacheGeneration uint64 `bin:"off=22b, siz=8, desc=cache_generation"` + UUIDTreeGeneration uint64 `bin:"off=233, siz=8, desc=uuid_tree_generation"` + Reserved [0xf0]byte `bin:"off=23b, siz=f0, desc=reserved /* future expansion */"` + SysChunkArray [0x800]byte `bin:"off=32b, siz=800, desc=sys_chunk_array:(n bytes valid) Contains (KEY . CHUNK_ITEM) pairs for all SYSTEM chunks. This is needed to bootstrap the mapping from logical addresses to physical. "` + SuperRoots [0x2a0]byte `bin:"off=b2b, siz=2a0, desc=Contain super_roots (4 btrfs_root_backup)"` + Unused [0x235]byte `bin:"off=dcb, siz=235, desc=current unused"` + + binstruct.End `bin:"off=1000"` +} + +type DevItem struct { + DeviceID uint64 `bin:"off=0, siz=8, desc=device id"` + + NumBytes uint64 `bin:"off=8, siz=8, desc=number of bytes"` + NumBytesUsed uint64 `bin:"off=10, siz=8, desc=number of bytes used"` + + IOOptimalAlign uint32 `bin:"off=18, siz=4, desc=optimal I/O align"` + IOOptimalWidth uint32 `bin:"off=1c, siz=4, desc=optimal I/O width"` + IOMinSize uint32 `bin:"off=20, siz=4, desc=minimal I/O size (sector size)"` + + Type uint64 `bin:"off=24, siz=8, desc=type"` + Generation uint64 `bin:"off=2c, siz=8, desc=generation"` + StartOffset uint64 `bin:"off=34, siz=8, desc=start offset"` + DevGroup uint32 `bin:"off=3c, siz=4, desc=dev group"` + SeekSpeed uint8 `bin:"off=40, siz=1, desc=seek speed"` + Bandwidth uint8 `bin:"off=41, siz=1, desc=bandwidth"` + + DevUUID UUID `bin:"off=42, siz=10, desc=device UUID"` + FSUUID UUID `bin:"off=52, siz=10, desc=FS UUID"` + + binstruct.End `bin:"off=62"` +} @@ -0,0 +1,5 @@ +module lukeshu.com/btrfs-tools + +go 1.18 + +require github.com/stretchr/testify v1.7.1 @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/binstruct/binstruct_test.go b/pkg/binstruct/binstruct_test.go new file mode 100644 index 0000000..33a2f0a --- /dev/null +++ b/pkg/binstruct/binstruct_test.go @@ -0,0 +1,49 @@ +package binstruct_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "lukeshu.com/btrfs-tools/pkg/binstruct" +) + +func TestSmoke(t *testing.T) { + type UUID [16]byte + type DevItem struct { + DeviceID uint64 `bin:"off=0, siz=8, desc=device id"` + + NumBytes uint64 `bin:"off=8, siz=8, desc=number of bytes"` + NumBytesUsed uint64 `bin:"off=10, siz=8, desc=number of bytes used"` + + IOOptimalAlign uint32 `bin:"off=18, siz=4, desc=optimal I/O align"` + IOOptimalWidth uint32 `bin:"off=1c, siz=4, desc=optimal I/O width"` + IOMinSize uint32 `bin:"off=20, siz=4, desc=minimal I/O size (sector size)"` + + Type uint64 `bin:"off=24, siz=8, desc=type"` + Generation uint64 `bin:"off=2c, siz=8, desc=generation"` + StartOffset uint64 `bin:"off=34, siz=8, desc=start offset"` + DevGroup uint32 `bin:"off=3c, siz=4, desc=dev group"` + SeekSpeed uint8 `bin:"off=40, siz=1, desc=seek speed"` + Bandwidth uint8 `bin:"off=41, siz=1, desc=bandwidth"` + + DevUUID UUID `bin:"off=42, siz=10, desc=device UUID"` + FSUUID UUID `bin:"off=52, siz=10, desc=FS UUID"` + + binstruct.End `bin:"off=62"` + } + type TestType struct { + Magic [5]byte `bin:"off=0,siz=5"` + Dev DevItem `bin:"off=5,siz=62"` + + binstruct.End `bin:"off=67"` + } + + input := TestType{} + copy(input.Magic[:], "mAgIc") + input.Dev.DeviceID = 12 + + bs, err := binstruct.Marshal(input) + assert.NoError(t, err) + assert.True(t, len(bs) == 0x67, "len(bs)=0x%x", len(bs)) +} diff --git a/pkg/binstruct/l1.go b/pkg/binstruct/l1.go new file mode 100644 index 0000000..c535d2e --- /dev/null +++ b/pkg/binstruct/l1.go @@ -0,0 +1,163 @@ +package binstruct + +import ( + "encoding/binary" + "fmt" + "reflect" +) + +type handler interface { + Unmarshal(dat []byte) interface{} + Marshal(val interface{}) []byte + Size() int64 +} + +type primitive struct { + unmarshal func(dat []byte) interface{} + marshal func(val interface{}) []byte + size int64 +} + +func (p primitive) Unmarshal(dat []byte) interface{} { return p.unmarshal(dat) } +func (p primitive) Marshal(val interface{}) []byte { return p.marshal(val) } +func (p primitive) Size() int64 { return p.size } + +var _ handler = primitive{} + +func genHandler(typ reflect.Type) (handler, error) { + switch typ.Kind() { + case reflect.Invalid: // invalid + return nil, fmt.Errorf("unsupported kind: %s: %v", typ.Kind(), typ) + case reflect.Bool, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128: // I don't wanna + return nil, fmt.Errorf("unsupported kind: %s: %v", typ.Kind(), typ) + case reflect.Int, reflect.Uint, reflect.Uintptr: // platform specific + return nil, fmt.Errorf("unsupported kind: %s: %v", typ.Kind(), typ) + case reflect.Chan, reflect.Func, reflect.Interface, reflect.UnsafePointer: // runtime + return nil, fmt.Errorf("unsupported kind: %s: %v", typ.Kind(), typ) + case reflect.Map, reflect.Slice, reflect.String: // dynamic size + return nil, fmt.Errorf("unsupported kind: %s: %v", typ.Kind(), typ) + + // uint //////////////////////////////////////////////////////////////// + case reflect.Uint8: + return primitive{ + unmarshal: func(dat []byte) interface{} { return dat[0] }, + marshal: func(val interface{}) []byte { return []byte{val.(uint8)} }, + size: 1, + }, nil + case reflect.Uint16: + return primitive{ + unmarshal: func(dat []byte) interface{} { return binary.LittleEndian.Uint16(dat) }, + marshal: func(val interface{}) []byte { + var buf [2]byte + binary.LittleEndian.PutUint16(buf[:], val.(uint16)) + return buf[:] + }, + size: 2, + }, nil + case reflect.Uint32: + return primitive{ + unmarshal: func(dat []byte) interface{} { return binary.LittleEndian.Uint32(dat) }, + marshal: func(val interface{}) []byte { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], val.(uint32)) + return buf[:] + }, + size: 4, + }, nil + case reflect.Uint64: + return primitive{ + unmarshal: func(dat []byte) interface{} { return binary.LittleEndian.Uint64(dat) }, + marshal: func(val interface{}) []byte { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], val.(uint64)) + return buf[:] + }, + size: 8, + }, nil + + // int ///////////////////////////////////////////////////////////////// + case reflect.Int8: + return primitive{ + unmarshal: func(dat []byte) interface{} { return int8(dat[0]) }, + marshal: func(val interface{}) []byte { return []byte{uint8(val.(int8))} }, + size: 1, + }, nil + case reflect.Int16: + return primitive{ + unmarshal: func(dat []byte) interface{} { return int16(binary.LittleEndian.Uint16(dat)) }, + marshal: func(val interface{}) []byte { + var buf [2]byte + binary.LittleEndian.PutUint16(buf[:], uint16(val.(int16))) + return buf[:] + }, + size: 2, + }, nil + case reflect.Int32: + return primitive{ + unmarshal: func(dat []byte) interface{} { return int32(binary.LittleEndian.Uint32(dat)) }, + marshal: func(val interface{}) []byte { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], uint32(val.(int32))) + return buf[:] + }, + size: 4, + }, nil + case reflect.Int64: + return primitive{ + unmarshal: func(dat []byte) interface{} { return int64(binary.LittleEndian.Uint64(dat)) }, + marshal: func(val interface{}) []byte { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], uint64(val.(int64))) + return buf[:] + }, + size: 8, + }, nil + + // composite /////////////////////////////////////////////////////////// + + case reflect.Ptr: + inner, err := getHandler(typ.Elem()) + if err != nil { + return nil, fmt.Errorf("%v: %w", typ, err) + } + return primitive{ + unmarshal: func(dat []byte) interface{} { + return reflect.ValueOf(inner.Unmarshal(dat)).Addr().Interface() + }, + marshal: func(val interface{}) []byte { + return inner.Marshal(reflect.ValueOf(val).Elem().Interface()) + }, + size: inner.Size(), + }, nil + case reflect.Array: + inner, err := getHandler(typ.Elem()) + if err != nil { + return nil, fmt.Errorf("%v: %w", typ, err) + } + return primitive{ + unmarshal: func(dat []byte) interface{} { + val := reflect.Zero(typ) + for i := 0; i < typ.Len(); i++ { + fmt.Printf("%v[%d]: %v\n", typ, i, val.Index(i)) + val.Index(i).Set(reflect.ValueOf(inner.Unmarshal(dat[i*int(inner.Size()):]))) + } + return val.Interface() + }, + marshal: func(val interface{}) []byte { + _val := reflect.ValueOf(val) + var ret []byte + for i := 0; i < typ.Len(); i++ { + ret = append(ret, inner.Marshal(_val.Index(i).Interface())...) + } + return ret + }, + size: inner.Size() * int64(typ.Len()), + }, nil + case reflect.Struct: + return genStructHandler(typ) + + // end ///////////////////////////////////////////////////////////////// + default: + panic(fmt.Errorf("unknown kind: %v: %v", typ.Kind(), typ)) + } +} diff --git a/pkg/binstruct/l2.go b/pkg/binstruct/l2.go new file mode 100644 index 0000000..005d686 --- /dev/null +++ b/pkg/binstruct/l2.go @@ -0,0 +1,156 @@ +package binstruct + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +type End struct{} + +type structHandler struct { + typ reflect.Type + fields []structField + size int64 +} + +type structField struct { + tag + handler + name string +} + +func (sh structHandler) Unmarshal(dat []byte) interface{} { + val := reflect.Zero(sh.typ) + for i, field := range sh.fields { + if field.skip { + continue + } + val.Field(i).Set(reflect.ValueOf(field.Unmarshal(dat[field.off:]))) + } + return val.Interface() +} +func (sh structHandler) Marshal(_val interface{}) []byte { + val := reflect.ValueOf(_val) + ret := make([]byte, 0, sh.size) + for i, field := range sh.fields { + if field.skip { + continue + } + if int64(len(ret)) != field.off { + panic(fmt.Errorf("field %d %q: len(ret)=0x%x but field.off=0x%x", i, field.name, len(ret), field.off)) + } + ret = append(ret, field.Marshal(val.Field(i).Interface())...) + } + return ret +} +func (sh structHandler) Size() int64 { + return sh.size +} + +var _ handler = structHandler{} + +func genStructHandler(structInfo reflect.Type) (handler, error) { + ret := structHandler{ + typ: structInfo, + } + + var curOffset, endOffset int64 + for i := 0; i < structInfo.NumField(); i++ { + var fieldInfo reflect.StructField = structInfo.Field(i) + + fieldTag, err := parseStructTag(fieldInfo.Tag.Get("bin")) + if err != nil { + return nil, fmt.Errorf("%v: field %q: %w", + structInfo, fieldInfo.Name, err) + } + if fieldTag.skip { + ret.fields = append(ret.fields, structField{ + tag: fieldTag, + name: fieldInfo.Name, + }) + continue + } + + if fieldTag.off != curOffset { + err := fmt.Errorf("tag says off=0x%x but curOffset=0x%x", fieldTag.off, curOffset) + return nil, fmt.Errorf("%v: field %q: %w", + structInfo, fieldInfo.Name, err) + } + if fieldInfo.Type == reflect.TypeOf(End{}) { + endOffset = curOffset + } + + fieldHandler, err := getHandler(fieldInfo.Type) + if err != nil { + return nil, fmt.Errorf("%v: field %q: %w", + structInfo, fieldInfo.Name, err) + } + + if fieldTag.siz != fieldHandler.Size() { + err := fmt.Errorf("tag says siz=0x%x but handler.Size()=0x%x", fieldTag.siz, fieldHandler.Size()) + return nil, fmt.Errorf("%v: field %q: %w", + structInfo, fieldInfo.Name, err) + } + curOffset += fieldTag.siz + + ret.fields = append(ret.fields, structField{ + tag: fieldTag, + handler: fieldHandler, + name: fieldInfo.Name, + }) + } + ret.size = curOffset + + if ret.size != endOffset { + return nil, fmt.Errorf("%v: .size=%v but endOffset=%v", + structInfo, ret.size, endOffset) + } + + return ret, nil +} + +type tag struct { + skip bool + + off int64 + siz int64 + desc string +} + +func parseStructTag(str string) (tag, error) { + var ret tag + for _, part := range strings.Split(str, ",") { + part = strings.TrimSpace(part) + if part == "" { + continue + } + if part == "-" { + return tag{skip: true}, nil + } + keyval := strings.SplitN(part, "=", 2) + if len(keyval) != 2 { + return tag{}, fmt.Errorf("option is not a key=value pair: %q", part) + } + key := keyval[0] + val := keyval[1] + switch key { + case "off": + vint, err := strconv.ParseInt(val, 16, 64) + if err != nil { + return tag{}, err + } + ret.off = vint + case "siz": + vint, err := strconv.ParseInt(val, 16, 64) + if err != nil { + return tag{}, err + } + ret.siz = vint + case "desc": + ret.desc = val + } + } + return ret, nil +} diff --git a/pkg/binstruct/l3.go b/pkg/binstruct/l3.go new file mode 100644 index 0000000..f9fb8b1 --- /dev/null +++ b/pkg/binstruct/l3.go @@ -0,0 +1,61 @@ +package binstruct + +import ( + "fmt" + "reflect" +) + +var handlerCache = make(map[reflect.Type]handler) + +func getHandler(typ reflect.Type) (handler, error) { + h, ok := handlerCache[typ] + if ok { + return h, nil + } + + h, err := genHandler(typ) + if err != nil { + return nil, err + } + handlerCache[typ] = h + return h, nil +} + +func Unmarshal(dat []byte, dst interface{}) error { + _dst := reflect.ValueOf(dst) + if _dst.Kind() != reflect.Ptr { + return fmt.Errorf("not a pointer: %v", _dst.Type()) + } + handler, err := getHandler(_dst.Type().Elem()) + if err != nil { + return err + } + if int64(len(dat)) < handler.Size() { + return fmt.Errorf("need at least %d bytes of data, only have %d", + handler.Size(), len(dat)) + } + val := handler.Unmarshal(dat[:handler.Size()]) + _dst.Elem().Set(reflect.ValueOf(val)) + return nil +} + +func Marshal(val interface{}) ([]byte, error) { + handler, err := getHandler(reflect.TypeOf(val)) + if err != nil { + return nil, err + } + bs := handler.Marshal(val) + if int64(len(bs)) != handler.Size() { + return nil, fmt.Errorf("got %d bytes but expected %d bytes", + len(bs), handler.Size()) + } + return bs, nil +} + +func Size(val interface{}) (int64, error) { + handler, err := getHandler(reflect.TypeOf(val)) + if err != nil { + return 0, err + } + return handler.Size(), nil +} |