summaryrefslogtreecommitdiff
path: root/lib/btrfs/btrfsvol
diff options
context:
space:
mode:
Diffstat (limited to 'lib/btrfs/btrfsvol')
-rw-r--r--lib/btrfs/btrfsvol/addr.go47
-rw-r--r--lib/btrfs/btrfsvol/addr_test.go36
-rw-r--r--lib/btrfs/btrfsvol/blockgroupflags.go51
-rw-r--r--lib/btrfs/btrfsvol/chunk.go96
-rw-r--r--lib/btrfs/btrfsvol/devext.go89
-rw-r--r--lib/btrfs/btrfsvol/lvm.go355
6 files changed, 674 insertions, 0 deletions
diff --git a/lib/btrfs/btrfsvol/addr.go b/lib/btrfs/btrfsvol/addr.go
new file mode 100644
index 0000000..6c67826
--- /dev/null
+++ b/lib/btrfs/btrfsvol/addr.go
@@ -0,0 +1,47 @@
+package btrfsvol
+
+import (
+ "fmt"
+
+ "git.lukeshu.com/btrfs-progs-ng/lib/util"
+)
+
+type (
+ PhysicalAddr int64
+ LogicalAddr int64
+ AddrDelta int64
+)
+
+func formatAddr(addr int64, f fmt.State, verb rune) {
+ switch verb {
+ case 'v', 's', 'q':
+ str := fmt.Sprintf("%#016x", addr)
+ fmt.Fprintf(f, util.FmtStateString(f, verb), str)
+ default:
+ fmt.Fprintf(f, util.FmtStateString(f, verb), addr)
+ }
+}
+
+func (a PhysicalAddr) Format(f fmt.State, verb rune) { formatAddr(int64(a), f, verb) }
+func (a LogicalAddr) Format(f fmt.State, verb rune) { formatAddr(int64(a), f, verb) }
+func (d AddrDelta) Format(f fmt.State, verb rune) { formatAddr(int64(d), f, verb) }
+
+func (a PhysicalAddr) Sub(b PhysicalAddr) AddrDelta { return AddrDelta(a - b) }
+func (a LogicalAddr) Sub(b LogicalAddr) AddrDelta { return AddrDelta(a - b) }
+
+func (a PhysicalAddr) Add(b AddrDelta) PhysicalAddr { return a + PhysicalAddr(b) }
+func (a LogicalAddr) Add(b AddrDelta) LogicalAddr { return a + LogicalAddr(b) }
+
+type DeviceID uint64
+
+type QualifiedPhysicalAddr struct {
+ Dev DeviceID
+ Addr PhysicalAddr
+}
+
+func (a QualifiedPhysicalAddr) Cmp(b QualifiedPhysicalAddr) int {
+ if d := int(a.Dev - b.Dev); d != 0 {
+ return d
+ }
+ return int(a.Addr - b.Addr)
+}
diff --git a/lib/btrfs/btrfsvol/addr_test.go b/lib/btrfs/btrfsvol/addr_test.go
new file mode 100644
index 0000000..aae6378
--- /dev/null
+++ b/lib/btrfs/btrfsvol/addr_test.go
@@ -0,0 +1,36 @@
+package btrfsvol_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "git.lukeshu.com/btrfs-progs-ng/lib/btrfs/btrfsvol"
+)
+
+func TestAddrFormat(t *testing.T) {
+ t.Parallel()
+ type TestCase struct {
+ InputAddr btrfsvol.LogicalAddr
+ InputFmt string
+ Output string
+ }
+ addr := btrfsvol.LogicalAddr(0x3a41678000)
+ testcases := map[string]TestCase{
+ "v": {InputAddr: addr, InputFmt: "%v", Output: "0x0000003a41678000"},
+ "s": {InputAddr: addr, InputFmt: "%s", Output: "0x0000003a41678000"},
+ "q": {InputAddr: addr, InputFmt: "%q", Output: `"0x0000003a41678000"`},
+ "x": {InputAddr: addr, InputFmt: "%x", Output: "3a41678000"},
+ "d": {InputAddr: addr, InputFmt: "%d", Output: "250205405184"},
+ "neg": {InputAddr: -1, InputFmt: "%v", Output: "-0x000000000000001"},
+ }
+ for tcName, tc := range testcases {
+ tc := tc
+ t.Run(tcName, func(t *testing.T) {
+ t.Parallel()
+ actual := fmt.Sprintf(tc.InputFmt, tc.InputAddr)
+ assert.Equal(t, tc.Output, actual)
+ })
+ }
+}
diff --git a/lib/btrfs/btrfsvol/blockgroupflags.go b/lib/btrfs/btrfsvol/blockgroupflags.go
new file mode 100644
index 0000000..1a194d7
--- /dev/null
+++ b/lib/btrfs/btrfsvol/blockgroupflags.go
@@ -0,0 +1,51 @@
+package btrfsvol
+
+import (
+ "git.lukeshu.com/btrfs-progs-ng/lib/util"
+)
+
+type BlockGroupFlags uint64
+
+const (
+ BLOCK_GROUP_DATA = BlockGroupFlags(1 << iota)
+ BLOCK_GROUP_SYSTEM
+ BLOCK_GROUP_METADATA
+ BLOCK_GROUP_RAID0
+ BLOCK_GROUP_RAID1
+ BLOCK_GROUP_DUP
+ BLOCK_GROUP_RAID10
+ BLOCK_GROUP_RAID5
+ BLOCK_GROUP_RAID6
+ BLOCK_GROUP_RAID1C3
+ BLOCK_GROUP_RAID1C4
+
+ BLOCK_GROUP_RAID_MASK = (BLOCK_GROUP_RAID1 | BLOCK_GROUP_DUP | BLOCK_GROUP_RAID10 | BLOCK_GROUP_RAID5 | BLOCK_GROUP_RAID6 | BLOCK_GROUP_RAID1C3 | BLOCK_GROUP_RAID1C4)
+)
+
+var blockGroupFlagNames = []string{
+ "DATA",
+ "SYSTEM",
+ "METADATA",
+
+ "RAID0",
+ "RAID1",
+ "DUP",
+ "RAID10",
+ "RAID5",
+ "RAID6",
+ "RAID1C3",
+ "RAID1C4",
+}
+
+func (f BlockGroupFlags) Has(req BlockGroupFlags) bool { return f&req == req }
+func (f BlockGroupFlags) String() string {
+ ret := util.BitfieldString(f, blockGroupFlagNames, util.HexNone)
+ if f&BLOCK_GROUP_RAID_MASK == 0 {
+ if ret == "" {
+ ret = "single"
+ } else {
+ ret += "|single"
+ }
+ }
+ return ret
+}
diff --git a/lib/btrfs/btrfsvol/chunk.go b/lib/btrfs/btrfsvol/chunk.go
new file mode 100644
index 0000000..6aea483
--- /dev/null
+++ b/lib/btrfs/btrfsvol/chunk.go
@@ -0,0 +1,96 @@
+package btrfsvol
+
+import (
+ "fmt"
+ "sort"
+
+ "git.lukeshu.com/btrfs-progs-ng/lib/util"
+)
+
+// logical => []physical
+type chunkMapping struct {
+ LAddr LogicalAddr
+ PAddrs []QualifiedPhysicalAddr
+ Size AddrDelta
+ SizeLocked bool
+ Flags *BlockGroupFlags
+}
+
+type ChunkMapping = chunkMapping
+
+// return -1 if 'a' is wholly to the left of 'b'
+// return 0 if there is some overlap between 'a' and 'b'
+// return 1 if 'a is wholly to the right of 'b'
+func (a chunkMapping) cmpRange(b chunkMapping) int {
+ switch {
+ case a.LAddr.Add(a.Size) <= b.LAddr:
+ // 'a' is wholly to the left of 'b'.
+ return -1
+ case b.LAddr.Add(b.Size) <= a.LAddr:
+ // 'a' is wholly to the right of 'b'.
+ return 1
+ default:
+ // There is some overlap.
+ return 0
+ }
+}
+
+func (a chunkMapping) union(rest ...chunkMapping) (chunkMapping, error) {
+ // sanity check
+ for _, chunk := range rest {
+ if a.cmpRange(chunk) != 0 {
+ return chunkMapping{}, fmt.Errorf("chunks don't overlap")
+ }
+ }
+ chunks := append([]chunkMapping{a}, rest...)
+ // figure out the logical range (.LAddr and .Size)
+ beg := chunks[0].LAddr
+ end := chunks[0].LAddr.Add(chunks[0].Size)
+ for _, chunk := range chunks {
+ beg = util.Min(beg, chunk.LAddr)
+ end = util.Max(end, chunk.LAddr.Add(chunk.Size))
+ }
+ ret := chunkMapping{
+ LAddr: beg,
+ Size: end.Sub(beg),
+ }
+ for _, chunk := range chunks {
+ if chunk.SizeLocked {
+ ret.SizeLocked = true
+ if ret.Size != chunk.Size {
+ return chunkMapping{}, fmt.Errorf("member chunk has locked size=%v, but union would have size=%v",
+ chunk.Size, ret.Size)
+ }
+ }
+ }
+ // figure out the physical stripes (.PAddrs)
+ paddrs := make(map[QualifiedPhysicalAddr]struct{})
+ for _, chunk := range chunks {
+ offsetWithinRet := chunk.LAddr.Sub(ret.LAddr)
+ for _, stripe := range chunk.PAddrs {
+ paddrs[QualifiedPhysicalAddr{
+ Dev: stripe.Dev,
+ Addr: stripe.Addr.Add(-offsetWithinRet),
+ }] = struct{}{}
+ }
+ }
+ ret.PAddrs = util.MapKeys(paddrs)
+ sort.Slice(ret.PAddrs, func(i, j int) bool {
+ return ret.PAddrs[i].Cmp(ret.PAddrs[j]) < 0
+ })
+ // figure out the flags (.Flags)
+ for _, chunk := range chunks {
+ if chunk.Flags == nil {
+ continue
+ }
+ if ret.Flags == nil {
+ val := *chunk.Flags
+ ret.Flags = &val
+ }
+ if *ret.Flags != *chunk.Flags {
+ return ret, fmt.Errorf("mismatch flags: %v != %v", *ret.Flags, *chunk.Flags)
+ }
+ }
+ // done
+ return ret, nil
+}
diff --git a/lib/btrfs/btrfsvol/devext.go b/lib/btrfs/btrfsvol/devext.go
new file mode 100644
index 0000000..43c6255
--- /dev/null
+++ b/lib/btrfs/btrfsvol/devext.go
@@ -0,0 +1,89 @@
+package btrfsvol
+
+import (
+ "fmt"
+
+ "git.lukeshu.com/btrfs-progs-ng/lib/util"
+)
+
+// physical => logical
+type devextMapping struct {
+ PAddr PhysicalAddr
+ LAddr LogicalAddr
+ Size AddrDelta
+ SizeLocked bool
+ Flags *BlockGroupFlags
+}
+
+// return -1 if 'a' is wholly to the left of 'b'
+// return 0 if there is some overlap between 'a' and 'b'
+// return 1 if 'a is wholly to the right of 'b'
+func (a devextMapping) cmpRange(b devextMapping) int {
+ switch {
+ case a.PAddr.Add(a.Size) <= b.PAddr:
+ // 'a' is wholly to the left of 'b'.
+ return -1
+ case b.PAddr.Add(b.Size) <= a.PAddr:
+ // 'a' is wholly to the right of 'b'.
+ return 1
+ default:
+ // There is some overlap.
+ return 0
+ }
+}
+
+func (a devextMapping) union(rest ...devextMapping) (devextMapping, error) {
+ // sanity check
+ for _, ext := range rest {
+ if a.cmpRange(ext) != 0 {
+ return devextMapping{}, fmt.Errorf("devexts don't overlap")
+ }
+ }
+ exts := append([]devextMapping{a}, rest...)
+ // figure out the physical range (.PAddr and .Size)
+ beg := exts[0].PAddr
+ end := beg.Add(exts[0].Size)
+ for _, ext := range exts {
+ beg = util.Min(beg, ext.PAddr)
+ end = util.Max(end, ext.PAddr.Add(ext.Size))
+ }
+ ret := devextMapping{
+ PAddr: beg,
+ Size: end.Sub(beg),
+ }
+ for _, ext := range exts {
+ if ext.SizeLocked {
+ ret.SizeLocked = true
+ if ret.Size != ext.Size {
+ return devextMapping{}, fmt.Errorf("member devext has locked size=%v, but union would have size=%v",
+ ext.Size, ret.Size)
+ }
+ }
+ }
+ // figure out the logical range (.LAddr)
+ first := true
+ for _, ext := range exts {
+ offsetWithinRet := ext.PAddr.Sub(ret.PAddr)
+ laddr := ext.LAddr.Add(-offsetWithinRet)
+ if first {
+ ret.LAddr = laddr
+ } else if laddr != ret.LAddr {
+ return ret, fmt.Errorf("devexts don't agree on laddr: %v != %v", ret.LAddr, laddr)
+ }
+ }
+ // figure out the flags (.Flags)
+ for _, ext := range exts {
+ if ext.Flags == nil {
+ continue
+ }
+ if ret.Flags == nil {
+ val := *ext.Flags
+ ret.Flags = &val
+ }
+ if *ret.Flags != *ext.Flags {
+ return ret, fmt.Errorf("mismatch flags: %v != %v", *ret.Flags, *ext.Flags)
+ }
+ }
+ // done
+ return ret, nil
+}
diff --git a/lib/btrfs/btrfsvol/lvm.go b/lib/btrfs/btrfsvol/lvm.go
new file mode 100644
index 0000000..3b9ccf6
--- /dev/null
+++ b/lib/btrfs/btrfsvol/lvm.go
@@ -0,0 +1,355 @@
+package btrfsvol
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "reflect"
+
+ "git.lukeshu.com/btrfs-progs-ng/lib/rbtree"
+ "git.lukeshu.com/btrfs-progs-ng/lib/util"
+)
+
+type LogicalVolume[PhysicalVolume util.File[PhysicalAddr]] struct {
+ name string
+
+ id2pv map[DeviceID]PhysicalVolume
+
+ logical2physical *rbtree.Tree[LogicalAddr, chunkMapping]
+ physical2logical map[DeviceID]*rbtree.Tree[PhysicalAddr, devextMapping]
+}
+
+var _ util.File[LogicalAddr] = (*LogicalVolume[util.File[PhysicalAddr]])(nil)
+
+func (lv *LogicalVolume[PhysicalVolume]) init() {
+ if lv.id2pv == nil {
+ lv.id2pv = make(map[DeviceID]PhysicalVolume)
+ }
+ if lv.logical2physical == nil {
+ lv.logical2physical = &rbtree.Tree[LogicalAddr, chunkMapping]{
+ KeyFn: func(chunk chunkMapping) LogicalAddr {
+ return chunk.LAddr
+ },
+ }
+ }
+ if lv.physical2logical == nil {
+ lv.physical2logical = make(map[DeviceID]*rbtree.Tree[PhysicalAddr, devextMapping], len(lv.id2pv))
+ }
+ for devid := range lv.id2pv {
+ if _, ok := lv.physical2logical[devid]; !ok {
+ lv.physical2logical[devid] = &rbtree.Tree[PhysicalAddr, devextMapping]{
+ KeyFn: func(ext devextMapping) PhysicalAddr {
+ return ext.PAddr
+ },
+ }
+ }
+ }
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) SetName(name string) {
+ lv.name = name
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) Name() string {
+ return lv.name
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) Size() (LogicalAddr, error) {
+ lv.init()
+ lastChunk := lv.logical2physical.Max()
+ if lastChunk == nil {
+ return 0, nil
+ }
+ return lastChunk.Value.LAddr.Add(lastChunk.Value.Size), nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) AddPhysicalVolume(id DeviceID, dev PhysicalVolume) error {
+ lv.init()
+ if other, exists := lv.id2pv[id]; exists {
+ return fmt.Errorf("(%p).AddPhysicalVolume: cannot add physical volume %q: already have physical volume %q with id=%v",
+ lv, dev.Name(), other.Name(), id)
+ }
+ lv.id2pv[id] = dev
+ lv.physical2logical[id] = &rbtree.Tree[PhysicalAddr, devextMapping]{
+ KeyFn: func(ext devextMapping) PhysicalAddr {
+ return ext.PAddr
+ },
+ }
+ return nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) PhysicalVolumes() map[DeviceID]PhysicalVolume {
+ dup := make(map[DeviceID]PhysicalVolume, len(lv.id2pv))
+ for k, v := range lv.id2pv {
+ dup[k] = v
+ }
+ return dup
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) ClearMappings() {
+ lv.logical2physical = nil
+ lv.physical2logical = nil
+}
+
+type Mapping struct {
+ LAddr LogicalAddr
+ PAddr QualifiedPhysicalAddr
+ Size AddrDelta
+ SizeLocked bool
+ Flags *BlockGroupFlags
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) AddMapping(m Mapping) error {
+ lv.init()
+ // sanity check
+ if _, haveDev := lv.id2pv[m.PAddr.Dev]; !haveDev {
+ return fmt.Errorf("(%p).AddMapping: do not have a physical volume with id=%v",
+ lv, m.PAddr.Dev)
+ }
+
+ // logical2physical
+ newChunk := chunkMapping{
+ LAddr: m.LAddr,
+ PAddrs: []QualifiedPhysicalAddr{m.PAddr},
+ Size: m.Size,
+ SizeLocked: m.SizeLocked,
+ Flags: m.Flags,
+ }
+ logicalOverlaps := lv.logical2physical.SearchRange(newChunk.cmpRange)
+ var err error
+ newChunk, err = newChunk.union(logicalOverlaps...)
+ if err != nil {
+ return fmt.Errorf("(%p).AddMapping: %w", lv, err)
+ }
+
+ // physical2logical
+ newExt := devextMapping{
+ PAddr: m.PAddr.Addr,
+ LAddr: m.LAddr,
+ Size: m.Size,
+ SizeLocked: m.SizeLocked,
+ Flags: m.Flags,
+ }
+ physicalOverlaps := lv.physical2logical[m.PAddr.Dev].SearchRange(newExt.cmpRange)
+ newExt, err = newExt.union(physicalOverlaps...)
+ if err != nil {
+ return fmt.Errorf("(%p).AddMapping: %w", lv, err)
+ }
+
+ // optimize
+ if len(logicalOverlaps) == 1 && reflect.DeepEqual(newChunk, logicalOverlaps[0]) &&
+ len(physicalOverlaps) == 1 && reflect.DeepEqual(newExt, physicalOverlaps[0]) {
+ return nil
+ }
+
+ // logical2physical
+ for _, chunk := range logicalOverlaps {
+ lv.logical2physical.Delete(chunk.LAddr)
+ }
+ lv.logical2physical.Insert(newChunk)
+
+ // physical2logical
+ for _, ext := range physicalOverlaps {
+ lv.physical2logical[m.PAddr.Dev].Delete(ext.PAddr)
+ }
+ lv.physical2logical[m.PAddr.Dev].Insert(newExt)
+
+ // sanity check
+ //
+ // This is in-theory unnescessary, but that assumes that I
+ // made no mistakes in my algorithm above.
+ if os.Getenv("PARANOID") != "" {
+ if err := lv.fsck(); err != nil {
+ return err
+ }
+ }
+
+ // done
+ return nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) fsck() error {
+ physical2logical := make(map[DeviceID]*rbtree.Tree[PhysicalAddr, devextMapping])
+ if err := lv.logical2physical.Walk(func(node *rbtree.Node[chunkMapping]) error {
+ chunk := node.Value
+ for _, stripe := range chunk.PAddrs {
+ if _, devOK := lv.id2pv[stripe.Dev]; !devOK {
+ return fmt.Errorf("(%p).fsck: chunk references physical volume %v which does not exist",
+ lv, stripe.Dev)
+ }
+ if _, exists := physical2logical[stripe.Dev]; !exists {
+ physical2logical[stripe.Dev] = &rbtree.Tree[PhysicalAddr, devextMapping]{
+ KeyFn: func(ext devextMapping) PhysicalAddr {
+ return ext.PAddr
+ },
+ }
+ }
+ physical2logical[stripe.Dev].Insert(devextMapping{
+ PAddr: stripe.Addr,
+ LAddr: chunk.LAddr,
+ Size: chunk.Size,
+ Flags: chunk.Flags,
+ })
+ }
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ if len(lv.physical2logical) != len(physical2logical) {
+ return fmt.Errorf("(%p).fsck: skew between chunk tree and devext tree",
+ lv)
+ }
+ for devid := range lv.physical2logical {
+ if !lv.physical2logical[devid].Equal(physical2logical[devid]) {
+ return fmt.Errorf("(%p).fsck: skew between chunk tree and devext tree",
+ lv)
+ }
+ }
+
+ return nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) Mappings() []Mapping {
+ var ret []Mapping
+ _ = lv.logical2physical.Walk(func(node *rbtree.Node[chunkMapping]) error {
+ chunk := node.Value
+ var flags *BlockGroupFlags
+ if chunk.Flags != nil {
+ val := *chunk.Flags
+ flags = &val
+ }
+ for _, slice := range chunk.PAddrs {
+ ret = append(ret, Mapping{
+ LAddr: chunk.LAddr,
+ PAddr: slice,
+ Size: chunk.Size,
+ Flags: flags,
+ })
+ }
+ return nil
+ })
+ return ret
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) Resolve(laddr LogicalAddr) (paddrs map[QualifiedPhysicalAddr]struct{}, maxlen AddrDelta) {
+ node := lv.logical2physical.Search(func(chunk chunkMapping) int {
+ return chunkMapping{LAddr: laddr, Size: 1}.cmpRange(chunk)
+ })
+ if node == nil {
+ return nil, 0
+ }
+
+ chunk := node.Value
+
+ offsetWithinChunk := laddr.Sub(chunk.LAddr)
+ paddrs = make(map[QualifiedPhysicalAddr]struct{})
+ maxlen = chunk.Size - offsetWithinChunk
+ for _, stripe := range chunk.PAddrs {
+ paddrs[QualifiedPhysicalAddr{
+ Dev: stripe.Dev,
+ Addr: stripe.Addr.Add(offsetWithinChunk),
+ }] = struct{}{}
+ }
+
+ return paddrs, maxlen
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) ResolveAny(laddr LogicalAddr, size AddrDelta) (LogicalAddr, QualifiedPhysicalAddr) {
+ node := lv.logical2physical.Search(func(chunk chunkMapping) int {
+ return chunkMapping{LAddr: laddr, Size: size}.cmpRange(chunk)
+ })
+ if node == nil {
+ return -1, QualifiedPhysicalAddr{0, -1}
+ }
+ return node.Value.LAddr, node.Value.PAddrs[0]
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) UnResolve(paddr QualifiedPhysicalAddr) LogicalAddr {
+ node := lv.physical2logical[paddr.Dev].Search(func(ext devextMapping) int {
+ return devextMapping{PAddr: paddr.Addr, Size: 1}.cmpRange(ext)
+ })
+ if node == nil {
+ return -1
+ }
+
+ ext := node.Value
+
+ offsetWithinExt := paddr.Addr.Sub(ext.PAddr)
+ return ext.LAddr.Add(offsetWithinExt)
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) ReadAt(dat []byte, laddr LogicalAddr) (int, error) {
+ done := 0
+ for done < len(dat) {
+ n, err := lv.maybeShortReadAt(dat[done:], laddr+LogicalAddr(done))
+ done += n
+ if err != nil {
+ return done, err
+ }
+ }
+ return done, nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) maybeShortReadAt(dat []byte, laddr LogicalAddr) (int, error) {
+ paddrs, maxlen := lv.Resolve(laddr)
+ if len(paddrs) == 0 {
+ return 0, fmt.Errorf("read: could not map logical address %v", laddr)
+ }
+ if AddrDelta(len(dat)) > maxlen {
+ dat = dat[:maxlen]
+ }
+
+ buf := make([]byte, len(dat))
+ first := true
+ for paddr := range paddrs {
+ dev, ok := lv.id2pv[paddr.Dev]
+ if !ok {
+ return 0, fmt.Errorf("device=%v does not exist", paddr.Dev)
+ }
+ if _, err := dev.ReadAt(buf, paddr.Addr); err != nil {
+ return 0, fmt.Errorf("read device=%v paddr=%v: %w", paddr.Dev, paddr.Addr, err)
+ }
+ if first {
+ copy(dat, buf)
+ } else {
+ if !bytes.Equal(dat, buf) {
+ return 0, fmt.Errorf("inconsistent stripes at laddr=%v len=%v", laddr, len(dat))
+ }
+ }
+ }
+ return len(dat), nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) WriteAt(dat []byte, laddr LogicalAddr) (int, error) {
+ done := 0
+ for done < len(dat) {
+ n, err := lv.maybeShortWriteAt(dat[done:], laddr+LogicalAddr(done))
+ done += n
+ if err != nil {
+ return done, err
+ }
+ }
+ return done, nil
+}
+
+func (lv *LogicalVolume[PhysicalVolume]) maybeShortWriteAt(dat []byte, laddr LogicalAddr) (int, error) {
+ paddrs, maxlen := lv.Resolve(laddr)
+ if len(paddrs) == 0 {
+ return 0, fmt.Errorf("write: could not map logical address %v", laddr)
+ }
+ if AddrDelta(len(dat)) > maxlen {
+ dat = dat[:maxlen]
+ }
+
+ for paddr := range paddrs {
+ dev, ok := lv.id2pv[paddr.Dev]
+ if !ok {
+ return 0, fmt.Errorf("device=%v does not exist", paddr.Dev)
+ }
+ if _, err := dev.WriteAt(dat, paddr.Addr); err != nil {
+ return 0, fmt.Errorf("write device=%v paddr=%v: %w", paddr.Dev, paddr.Addr, err)
+ }
+ }
+ return len(dat), nil
+}