m/p/blockdev: init
Adds blockdev, a package providing a Go interface for generic block
devices as well as an implementation of it for Linux and auxiliary
types.
This will replace most ad-hoc block device handling in the monorepo.
Change-Id: I3a4e3b7c31a8344f7859210bbb4942977d1ad1d2
Reviewed-on: https://review.monogon.dev/c/monogon/+/1871
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/pkg/blockdev/memory.go b/metropolis/pkg/blockdev/memory.go
new file mode 100644
index 0000000..193f93c
--- /dev/null
+++ b/metropolis/pkg/blockdev/memory.go
@@ -0,0 +1,128 @@
+package blockdev
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "math/bits"
+)
+
+// Memory is a memory-backed implementation of BlockDev. It is optimal
+// for testing and temporary use, as it is fast and platform-independent.
+type Memory struct {
+ blockSize int64
+ blockCount int64
+ data []byte
+}
+
+// NewMemory returns a new memory-backed block device with the given geometry.
+func NewMemory(blockSize, blockCount int64) (*Memory, error) {
+ if blockSize <= 0 {
+ return nil, errors.New("block size cannot be zero or negative")
+ }
+ if bits.OnesCount64(uint64(blockSize)) > 1 {
+ return nil, fmt.Errorf("block size must be a power of two (got %d)", blockSize)
+ }
+ if blockCount < 0 {
+ return nil, errors.New("block count cannot be negative")
+ }
+ return &Memory{
+ blockSize: blockSize,
+ blockCount: blockCount,
+ data: make([]byte, blockSize*blockCount),
+ }, nil
+}
+
+// MustNewMemory works exactly like NewMemory, but panics when NewMemory would
+// return an error. Intended for use in tests.
+func MustNewMemory(blockSize, blockCount int64) *Memory {
+ m, err := NewMemory(blockSize, blockCount)
+ if err != nil {
+ panic(err)
+ }
+ return m
+}
+
+func (m *Memory) ReadAt(p []byte, off int64) (int, error) {
+ devSize := m.blockSize * m.blockCount
+ if off > devSize {
+ return 0, io.EOF
+ }
+ // TODO: Alignment checks?
+ copy(p, m.data[off:])
+ n := len(m.data[off:])
+ if n < len(p) {
+ return n, io.EOF
+ }
+ return len(p), nil
+}
+
+func (m *Memory) WriteAt(p []byte, off int64) (int, error) {
+ devSize := m.blockSize * m.blockCount
+ if off > devSize {
+ return 0, io.EOF
+ }
+ // TODO: Alignment checks?
+ copy(m.data[off:], p)
+ n := len(m.data[off:])
+ if n < len(p) {
+ return n, io.EOF
+ }
+ return len(p), nil
+}
+
+func (m *Memory) BlockSize() int64 {
+ return m.blockSize
+}
+
+func (m *Memory) BlockCount() int64 {
+ return m.blockCount
+}
+
+func (m *Memory) OptimalBlockSize() int64 {
+ return m.blockSize
+}
+
+func (m *Memory) validRange(startByte, endByte int64) error {
+ if startByte > endByte {
+ return fmt.Errorf("startByte (%d) larger than endByte (%d), invalid interval", startByte, endByte)
+ }
+ devSize := m.blockSize * m.blockCount
+ if startByte >= devSize || startByte < 0 {
+ return fmt.Errorf("startByte (%d) out of range (0-%d)", endByte, devSize)
+ }
+ if endByte > devSize || endByte < 0 {
+ return fmt.Errorf("endByte (%d) out of range (0-%d)", endByte, devSize)
+ }
+ // Alignment check works for powers of two by looking at every bit below
+ // the bit set in the block size.
+ if startByte&(m.blockSize-1) != 0 {
+ return fmt.Errorf("startByte (%d) is not aligned to blockSize (%d)", startByte, m.blockSize)
+ }
+ if endByte&(m.blockSize-1) != 0 {
+ return fmt.Errorf("endByte (%d) is not aligned to blockSize (%d)", startByte, m.blockSize)
+ }
+ return nil
+}
+
+func (m *Memory) Discard(startByte, endByte int64) error {
+ if err := m.validRange(startByte, endByte); err != nil {
+ return err
+ }
+ for i := startByte; i < endByte; i++ {
+ // Intentionally don't set to zero as Discard doesn't guarantee
+ // any specific contents. Call Zero if you need this.
+ m.data[i] = 0xaa
+ }
+ return nil
+}
+
+func (m *Memory) Zero(startByte, endByte int64) error {
+ if err := m.validRange(startByte, endByte); err != nil {
+ return err
+ }
+ for i := startByte; i < endByte; i++ {
+ m.data[i] = 0x00
+ }
+ return nil
+}