diff --git a/btrfs_test.go b/btrfs_test.go index 067f4fc..95f5a41 100644 --- a/btrfs_test.go +++ b/btrfs_test.go @@ -5,6 +5,8 @@ import ( "io" "os" "path/filepath" + "reflect" + "sort" "testing" ) @@ -74,6 +76,70 @@ func TestIsSubvolume(t *testing.T) { mksub("d1/v2/v3") } +func TestSubvolumes(t *testing.T) { + dir, closer := btrfstest.New(t, sizeDef) + defer closer() + fs, err := Open(dir, false) + if err != nil { + t.Fatal(err) + } + defer fs.Close() + + mksub := func(in string, path string) { + if in != "" { + path = filepath.Join(dir, in, path) + } else { + path = filepath.Join(dir, path) + } + if err := CreateSubVolume(path); err != nil { + t.Fatalf("cannot create subvolume %v: %v", path, err) + } + } + delsub := func(path string) { + path = filepath.Join(dir, path) + if err := DeleteSubVolume(path); err != nil { + t.Fatalf("cannot delete subvolume %v: %v", path, err) + } + } + expect := func(exp []string) { + subs, err := fs.ListSubvolumes(nil) + if err != nil { + t.Fatal(err) + } + var got []string + for _, s := range subs { + if s.UUID.IsZero() { + t.Fatalf("zero uuid in %+v", s) + } + if s.Name != "" { + got = append(got, s.Name) + } + } + sort.Strings(got) + sort.Strings(exp) + if !reflect.DeepEqual(got, exp) { + t.Fatalf("list failed:\ngot: %v\nvs\nexp: %v", got, exp) + } + } + + names := []string{"foo", "bar", "baz"} + for _, name := range names { + mksub("", name) + } + for _, name := range names { + mksub(names[0], name) + } + expect([]string{ + "foo", "bar", "baz", + "foo", "bar", "baz", + }) + delsub("foo/bar") + expect([]string{ + "foo", "bar", "baz", + "foo", "baz", + }) +} + func TestCompression(t *testing.T) { dir, closer := btrfstest.New(t, sizeDef) defer closer() diff --git a/btrfs_tree.go b/btrfs_tree.go index 5d0ebca..2a9cdb0 100644 --- a/btrfs_tree.go +++ b/btrfs_tree.go @@ -37,11 +37,6 @@ func asUint16(p []byte) uint16 { return *(*uint16)(unsafe.Pointer(&p[0])) } -func asTime(p []byte) time.Time { - sec, nsec := asUint64(p[0:]), asUint32(p[8:]) - return time.Unix(int64(sec), int64(nsec)) -} - func asRootRef(p []byte) rootRef { const sz = 18 // assuming that it is highly unsafe to have sizeof(struct) > len(data) @@ -55,3 +50,204 @@ func asRootRef(p []byte) rootRef { } return ref } + +// btrfs_disk_key_raw is a raw bytes for btrfs_disk_key structure +type btrfs_disk_key_raw [17]byte + +func (p btrfs_disk_key_raw) Decode() diskKey { + return diskKey{ + ObjectID: asUint64(p[0:]), + Type: p[8], + Offset: asUint64(p[9:]), + } +} + +type diskKey struct { + ObjectID uint64 + Type byte + Offset uint64 +} + +// btrfs_timespec_raw is a raw bytes for btrfs_timespec structure. +type btrfs_timespec_raw [12]byte + +func (t btrfs_timespec_raw) Decode() time.Time { + sec, nsec := asUint64(t[0:]), asUint32(t[8:]) + return time.Unix(int64(sec), int64(nsec)) +} + +// timeBlock is a raw set of bytes for 4 time fields: +// atime btrfs_timespec +// ctime btrfs_timespec +// mtime btrfs_timespec +// otime btrfs_timespec +// It is used to keep correct alignment when accessing structures from btrfs. +type timeBlock [4]btrfs_timespec_raw + +type btrfs_inode_item_raw struct { + generation uint64 + transid uint64 + size uint64 + nbytes uint64 + block_group uint64 + nlink uint32 + uid uint32 + gid uint32 + mode uint32 + rdev uint64 + flags uint64 + sequence uint64 + _ [4]uint64 // reserved + // atime btrfs_timespec + // ctime btrfs_timespec + // mtime btrfs_timespec + // otime btrfs_timespec + times timeBlock +} + +func (v btrfs_inode_item_raw) Decode() inodeItem { + return inodeItem{ + Gen: v.generation, + TransID: v.transid, + Size: v.size, + NBytes: v.nbytes, + BlockGroup: v.block_group, + NLink: v.nlink, + UID: v.uid, + GID: v.gid, + Mode: v.mode, + RDev: v.rdev, + Flags: v.flags, + Sequence: v.sequence, + ATime: v.times[0].Decode(), + CTime: v.times[1].Decode(), + MTime: v.times[2].Decode(), + OTime: v.times[3].Decode(), + } +} + +type inodeItem struct { + Gen uint64 // nfs style generation number + TransID uint64 // transid that last touched this inode + Size uint64 + NBytes uint64 + BlockGroup uint64 + NLink uint32 + UID uint32 + GID uint32 + Mode uint32 + RDev uint64 + Flags uint64 + Sequence uint64 // modification sequence number for NFS + ATime time.Time + CTime time.Time + MTime time.Time + OTime time.Time +} + +func asRootItem(p []byte) *btrfs_root_item_raw { + return (*btrfs_root_item_raw)(unsafe.Pointer(&p[0])) +} + +type btrfs_root_item_raw [439]byte + +func (p btrfs_root_item_raw) Decode() rootItem { + const ( + off2 = unsafe.Sizeof(btrfs_root_item_raw_p1{}) + off3 = off2 + 23 + ) + p1 := (*btrfs_root_item_raw_p1)(unsafe.Pointer(&p[0])) + p2 := p[off2 : off2+23] + p2_k := (*btrfs_disk_key_raw)(unsafe.Pointer(&p[off2+4])) + p2_b := p2[4+17:] + p3 := (*btrfs_root_item_raw_p3)(unsafe.Pointer(&p[off3])) + return rootItem{ + Inode: p1.inode.Decode(), + Gen: p1.generation, + RootDirID: p1.root_dirid, + ByteNr: p1.bytenr, + ByteLimit: p1.byte_limit, + BytesUsed: p1.bytes_used, + LastSnapshot: p1.last_snapshot, + Flags: p1.flags, + // from here, Go structure become misaligned with C structure + Refs: asUint32(p2[0:]), + DropProgress: p2_k.Decode(), + DropLevel: p2_b[0], + Level: p2_b[1], + // these fields are still misaligned by 1 bytes + // TODO(dennwc): it's a copy of Gen to check structure version; hide it maybe? + GenV2: p3.generation_v2, + UUID: p3.uuid, + ParentUUID: p3.parent_uuid, + ReceivedUUID: p3.received_uuid, + CTransID: p3.ctransid, + OTransID: p3.otransid, + STransID: p3.stransid, + RTransID: p3.rtransid, + CTime: p3.times[0].Decode(), + OTime: p3.times[1].Decode(), + STime: p3.times[2].Decode(), + RTime: p3.times[3].Decode(), + } +} + +type rootItem struct { + Inode inodeItem + Gen uint64 + RootDirID uint64 + ByteNr uint64 + ByteLimit uint64 + BytesUsed uint64 + LastSnapshot uint64 + Flags uint64 + Refs uint32 + DropProgress diskKey + DropLevel uint8 + Level uint8 + GenV2 uint64 + UUID UUID + ParentUUID UUID + ReceivedUUID UUID + CTransID uint64 + OTransID uint64 + STransID uint64 + RTransID uint64 + CTime time.Time + OTime time.Time + STime time.Time + RTime time.Time +} + +type btrfs_root_item_raw_p1 struct { + inode btrfs_inode_item_raw + generation uint64 + root_dirid uint64 + bytenr uint64 + byte_limit uint64 + bytes_used uint64 + last_snapshot uint64 + flags uint64 +} +type btrfs_root_item_raw_p2 struct { + refs uint32 + drop_progress btrfs_disk_key_raw + drop_level uint8 + level uint8 +} +type btrfs_root_item_raw_p3 struct { + generation_v2 uint64 + uuid UUID + parent_uuid UUID + received_uuid UUID + ctransid uint64 + otransid uint64 + stransid uint64 + rtransid uint64 + // ctime btrfs_timespec + // otime btrfs_timespec + // stime btrfs_timespec + // rtime btrfs_timespec + times timeBlock + _ [8]uint64 // reserved +} diff --git a/errors.go b/errors.go index cda99f8..b852bcf 100644 --- a/errors.go +++ b/errors.go @@ -1,6 +1,9 @@ package btrfs -import "fmt" +import ( + "errors" + "fmt" +) type ErrNotBtrfs struct { Path string @@ -42,3 +45,8 @@ var errorString = map[ErrCode]string{ ErrDevOnlyWritable: "unable to remove the only writeable device", ErrDevExclRunInProgress: "add/delete/balance/replace/resize operation in progress", } + +var ( + ErrNotFound = errors.New("not found") + errNotImplemented = errors.New("not implemented") +) diff --git a/size_test.go b/size_test.go index edc114a..01c813c 100644 --- a/size_test.go +++ b/size_test.go @@ -3,6 +3,7 @@ package btrfs import ( "reflect" "testing" + "unsafe" ) var caseSizes = []struct { @@ -48,15 +49,21 @@ var caseSizes = []struct { {obj: btrfs_ioctl_received_subvol_args{}, size: 200}, {obj: btrfs_ioctl_send_args{}, size: 72}, + //{obj:btrfs_timespec{},size:12}, //{obj:btrfs_root_ref{},size:18}, //{obj:btrfs_root_item{},size:439}, + {obj: btrfs_root_item_raw{}, size: 439}, + {obj: btrfs_root_item_raw_p1{}, size: 439 - 23 - int(unsafe.Sizeof(btrfs_root_item_raw_p3{}))}, + {obj: btrfs_root_item_raw_p3{}, size: 439 - 23 - int(unsafe.Sizeof(btrfs_root_item_raw_p1{}))}, //{obj:btrfs_inode_item{},size:160}, + {obj: btrfs_inode_item_raw{}, size: 160}, + {obj: timeBlock{}, size: 4 * 12}, } func TestSizes(t *testing.T) { for _, c := range caseSizes { if sz := int(reflect.ValueOf(c.obj).Type().Size()); sz != c.size { - t.Fatalf("unexpected size of %T: %d", c.obj, sz) + t.Errorf("unexpected size of %T: %d (exp: %d)", c.obj, sz, c.size) } } } diff --git a/subvolume.go b/subvolume.go index bbd8792..b268440 100644 --- a/subvolume.go +++ b/subvolume.go @@ -146,6 +146,23 @@ func SnapshotSubVolume(subvol, dst string, ro bool) error { return nil } +func IsReadOnly(path string) (bool, error) { + f, err := GetFlags(path) + if err != nil { + return false, err + } + return f.ReadOnly(), nil +} + +func GetFlags(path string) (SubvolFlags, error) { + fs, err := Open(path, true) + if err != nil { + return 0, err + } + defer fs.Close() + return fs.GetFlags() +} + type Subvolume struct { ObjectID uint64 TransID uint64 @@ -205,18 +222,15 @@ func listSubVolumes(f *os.File) (map[uint64]Subvolume, error) { o := m[obj.ObjectID] o.TransID = obj.TransID o.ObjectID = obj.ObjectID - // TODO: decode whole object? - o.Gen = asUint64(obj.Data[160:]) // size of btrfs_inode_item - o.Flags = asUint64(obj.Data[160+6*8:]) - const sz = 439 - const toff = sz - 8*8 - 4*12 - o.CTime = asTime(obj.Data[toff+0*12:]) - o.OTime = asTime(obj.Data[toff+1*12:]) - o.OGen = asUint64(obj.Data[toff-3*8:]) - const uoff = toff - 4*8 - 3*UUIDSize - copy(o.UUID[:], obj.Data[uoff+0*UUIDSize:]) - copy(o.ParentUUID[:], obj.Data[uoff+1*UUIDSize:]) - copy(o.ReceivedUUID[:], obj.Data[uoff+2*UUIDSize:]) + robj := asRootItem(obj.Data).Decode() + o.Gen = robj.Gen + o.Flags = robj.Flags + o.CTime = robj.CTime + o.OTime = robj.OTime + o.OGen = robj.GenV2 + o.UUID = robj.UUID + o.ParentUUID = robj.ParentUUID + o.ReceivedUUID = robj.ReceivedUUID m[obj.ObjectID] = o } } @@ -243,3 +257,44 @@ func listSubVolumes(f *os.File) (map[uint64]Subvolume, error) { } return m, nil } + +type subvolInfo struct { + RootID uint64 + + UUID UUID + ParentUUID UUID + ReceivedUUID UUID + + CTransID uint64 + OTransID uint64 + STransID uint64 + RTransID uint64 + + Path string +} + +func subvolSearchByUUID(mnt *os.File, uuid UUID) (*subvolInfo, error) { + id, err := lookupUUIDSubvolItem(mnt, uuid) + if err != nil { + return nil, err + } + return subvolSearchByRootID(mnt, id) +} + +func subvolSearchByReceivedUUID(mnt *os.File, uuid UUID) (*subvolInfo, error) { + id, err := lookupUUIDReceivedSubvolItem(mnt, uuid) + if err != nil { + return nil, err + } + return subvolSearchByRootID(mnt, id) +} + +func subvolSearchByPath(mnt *os.File, path string) (*subvolInfo, error) { + var id uint64 + panic("not implemented") + return subvolSearchByRootID(mnt, id) +} + +func subvolSearchByRootID(mnt *os.File, rootID uint64) (*subvolInfo, error) { + panic("not implemented") +} diff --git a/utils.go b/utils.go index b4ebeca..b4c9408 100644 --- a/utils.go +++ b/utils.go @@ -18,19 +18,6 @@ func isBtrfs(path string) (bool, error) { return stfs.Type == SuperMagic, nil } -func IsReadOnly(path string) (bool, error) { - fs, err := Open(path, true) - if err != nil { - return false, err - } - defer fs.Close() - f, err := fs.GetFlags() - if err != nil { - return false, err - } - return f.ReadOnly(), nil -} - func findMountRoot(path string) (string, error) { mounts, err := mtab.Mounts() if err != nil { @@ -77,7 +64,7 @@ func openDir(path string) (*os.File, error) { return file, nil } -type rawItem struct { +type searchResult struct { TransID uint64 ObjectID uint64 Type uint32 @@ -85,24 +72,24 @@ type rawItem struct { Data []byte } -func treeSearchRaw(f *os.File, key btrfs_ioctl_search_key) (out []rawItem, _ error) { +func treeSearchRaw(mnt *os.File, key btrfs_ioctl_search_key) (out []searchResult, _ error) { args := btrfs_ioctl_search_args{ key: key, } - if err := iocTreeSearch(f, &args); err != nil { + if err := iocTreeSearch(mnt, &args); err != nil { return nil, err } - out = make([]rawItem, 0, args.key.nr_items) + out = make([]searchResult, 0, args.key.nr_items) buf := args.buf[:] for i := 0; i < int(args.key.nr_items); i++ { h := (*btrfs_ioctl_search_header)(unsafe.Pointer(&buf[0])) buf = buf[unsafe.Sizeof(btrfs_ioctl_search_header{}):] - out = append(out, rawItem{ + out = append(out, searchResult{ TransID: h.transid, ObjectID: h.objectid, - Type: h.typ, Offset: h.offset, - Data: buf[:h.len], // TODO: reallocate? + Type: h.typ, + Data: buf[:h.len:h.len], // TODO: reallocate? }) buf = buf[h.len:] } diff --git a/uuid_tree.go b/uuid_tree.go new file mode 100644 index 0000000..d3a1425 --- /dev/null +++ b/uuid_tree.go @@ -0,0 +1,49 @@ +package btrfs + +import ( + "encoding/binary" + "fmt" + "os" +) + +func lookupUUIDSubvolItem(f *os.File, uuid UUID) (uint64, error) { + return uuidTreeLookupAny(f, uuid, uuidKeySubvol) +} + +func lookupUUIDReceivedSubvolItem(f *os.File, uuid UUID) (uint64, error) { + return uuidTreeLookupAny(f, uuid, uuidKeyReceivedSubvol) +} + +func (id UUID) toKey() (objID, off uint64) { + objID = binary.LittleEndian.Uint64(id[:8]) + off = binary.LittleEndian.Uint64(id[8:16]) + return +} + +// uuidTreeLookupAny searches uuid tree for a given uuid in specified field. +// It returns ErrNotFound if object was not found. +func uuidTreeLookupAny(f *os.File, uuid UUID, typ uint32) (uint64, error) { + objId, off := uuid.toKey() + args := btrfs_ioctl_search_key{ + tree_id: uuidTreeObjectid, + min_objectid: objId, + max_objectid: objId, + min_type: typ, + max_type: typ, + min_offset: off, + max_offset: off, + max_transid: maxUint64, + nr_items: 1, + } + res, err := treeSearchRaw(f, args) + if err != nil { + return 0, err + } else if len(res) < 1 { + return 0, ErrNotFound + } + out := res[0] + if len(out.Data) != 8 { + return 0, fmt.Errorf("btrfs: uuid item with illegal size %d", len(out.Data)) + } + return binary.LittleEndian.Uint64(out.Data), nil +}