diff --git a/blockdev.go b/blockdev.go index 10e3edf..c8f2f3f 100644 --- a/blockdev.go +++ b/blockdev.go @@ -4,9 +4,6 @@ import ( "fmt" "io" "time" - - "github.com/codingminions/blockdev/internal/overlay" - "github.com/codingminions/blockdev/internal/validate" ) // BlockSize is 4096 to match the OS page size and NBD's expected sector size. @@ -18,10 +15,10 @@ const BlockSize = 4096 // after handoff; defensive copy would double peak memory when many sandboxes // share one image. type BlockDevice struct { - base []byte - length int64 - overlay *overlay.Store - cfg config + base []byte + length int64 + ov *overlay + cfg config } var ( @@ -33,18 +30,18 @@ var ( // base is a configuration bug, and we'd rather fail at construction than after // a sandbox has been wired up. func New(initial []byte, opts ...Option) (*BlockDevice, error) { - if err := validate.Alignment(0, len(initial)); err != nil { - return nil, fmt.Errorf("blockdev.New: len(initial)=%d: %w", len(initial), ErrMisaligned) + if err := checkAlignment(0, len(initial)); err != nil { + return nil, fmt.Errorf("blockdev.New: len(initial)=%d: %w", len(initial), err) } var cfg config for _, opt := range opts { opt(&cfg) } return &BlockDevice{ - base: initial, - length: int64(len(initial)), - overlay: overlay.New(), - cfg: cfg, + base: initial, + length: int64(len(initial)), + ov: newOverlay(), + cfg: cfg, }, nil } @@ -74,13 +71,13 @@ func (b *BlockDevice) ReadAt(p []byte, off int64) (int, error) { } func (b *BlockDevice) readAt(p []byte, off int64) (int, error) { - if err := validate.Bounds(off, len(p), b.length); err != nil { + if err := checkBounds(off, len(p), b.length); err != nil { return 0, fmt.Errorf("blockdev.ReadAt off=%d len=%d device=%d: %w", - off, len(p), b.length, ErrOutOfBounds) + off, len(p), b.length, err) } - if err := validate.Alignment(off, len(p)); err != nil { + if err := checkAlignment(off, len(p)); err != nil { return 0, fmt.Errorf("blockdev.ReadAt off=%d len=%d: %w", - off, len(p), ErrMisaligned) + off, len(p), err) } if len(p) == 0 { return 0, nil @@ -88,7 +85,7 @@ func (b *BlockDevice) readAt(p []byte, off int64) (int, error) { for i := 0; i < len(p); i += BlockSize { blockOff := off + int64(i) blockNum := blockOff / BlockSize - if data, ok := b.overlay.Get(blockNum); ok { + if data, ok := b.ov.get(blockNum); ok { copy(p[i:i+BlockSize], data) } else { copy(p[i:i+BlockSize], b.base[blockOff:blockOff+BlockSize]) @@ -120,20 +117,20 @@ func (b *BlockDevice) WriteAt(p []byte, off int64) (int, error) { } func (b *BlockDevice) writeAt(p []byte, off int64) (int, error) { - if err := validate.Bounds(off, len(p), b.length); err != nil { + if err := checkBounds(off, len(p), b.length); err != nil { return 0, fmt.Errorf("blockdev.WriteAt off=%d len=%d device=%d: %w", - off, len(p), b.length, ErrOutOfBounds) + off, len(p), b.length, err) } - if err := validate.Alignment(off, len(p)); err != nil { + if err := checkAlignment(off, len(p)); err != nil { return 0, fmt.Errorf("blockdev.WriteAt off=%d len=%d: %w", - off, len(p), ErrMisaligned) + off, len(p), err) } if len(p) == 0 { return 0, nil } for i := 0; i < len(p); i += BlockSize { blockNum := (off + int64(i)) / BlockSize - b.overlay.Put(blockNum, p[i:i+BlockSize]) + b.ov.put(blockNum, p[i:i+BlockSize]) } return len(p), nil } diff --git a/internal/overlay/overlay.go b/internal/overlay/overlay.go deleted file mode 100644 index b86438e..0000000 --- a/internal/overlay/overlay.go +++ /dev/null @@ -1,48 +0,0 @@ -// Package overlay is the copy-on-write block store: reads expose the stored -// slice (callers must not mutate), Puts copy so callers can reuse their buffer. -package overlay - -import "sync" - -type Store struct { - mu sync.RWMutex - m map[int64][]byte -} - -func New() *Store { - return &Store{m: make(map[int64][]byte)} -} - -// Returned slice aliases the store — caller must not mutate. -func (s *Store) Get(block int64) ([]byte, bool) { - s.mu.RLock() - defer s.mu.RUnlock() - data, ok := s.m[block] - return data, ok -} - -// Copies data so the caller can reuse its buffer immediately. -func (s *Store) Put(block int64, data []byte) { - buf := make([]byte, len(data)) - copy(buf, data) - s.mu.Lock() - s.m[block] = buf - s.mu.Unlock() -} - -// fn must not mutate data; return false to stop early. -func (s *Store) Range(fn func(block int64, data []byte) bool) { - s.mu.RLock() - defer s.mu.RUnlock() - for block, data := range s.m { - if !fn(block, data) { - return - } - } -} - -func (s *Store) Len() int { - s.mu.RLock() - defer s.mu.RUnlock() - return len(s.m) -} diff --git a/internal/validate/validate.go b/internal/validate/validate.go deleted file mode 100644 index 453073a..0000000 --- a/internal/validate/validate.go +++ /dev/null @@ -1,27 +0,0 @@ -// Package validate holds pure parameter predicates. Their errors are signals, -// not sentinels — callers wrap them with their own public errors so the -// internal package never appears in user-facing error chains. -package validate - -import "errors" - -const blockSize = 4096 - -var ( - errNotAligned = errors.New("validate: not block-aligned") - errOutOfRange = errors.New("validate: out of range") -) - -func Alignment(off int64, length int) error { - if off%blockSize != 0 || int64(length)%blockSize != 0 { - return errNotAligned - } - return nil -} - -func Bounds(off int64, length int, deviceSize int64) error { - if off < 0 || length < 0 || off+int64(length) > deviceSize { - return errOutOfRange - } - return nil -} diff --git a/overlay.go b/overlay.go new file mode 100644 index 0000000..7e98d8b --- /dev/null +++ b/overlay.go @@ -0,0 +1,48 @@ +package blockdev + +import "sync" + +// overlay is the copy-on-write block store: reads expose the stored slice +// (callers must not mutate), puts copy so callers can reuse their buffer. +type overlay struct { + mu sync.RWMutex + m map[int64][]byte +} + +func newOverlay() *overlay { + return &overlay{m: make(map[int64][]byte)} +} + +// Returned slice aliases the store — caller must not mutate. +func (o *overlay) get(block int64) ([]byte, bool) { + o.mu.RLock() + defer o.mu.RUnlock() + data, ok := o.m[block] + return data, ok +} + +// Copies data so the caller can reuse its buffer immediately. +func (o *overlay) put(block int64, data []byte) { + buf := make([]byte, len(data)) + copy(buf, data) + o.mu.Lock() + o.m[block] = buf + o.mu.Unlock() +} + +// fn must not mutate data; return false to stop early. +func (o *overlay) each(fn func(block int64, data []byte) bool) { + o.mu.RLock() + defer o.mu.RUnlock() + for block, data := range o.m { + if !fn(block, data) { + return + } + } +} + +func (o *overlay) count() int { + o.mu.RLock() + defer o.mu.RUnlock() + return len(o.m) +} diff --git a/validate.go b/validate.go new file mode 100644 index 0000000..7f22bf8 --- /dev/null +++ b/validate.go @@ -0,0 +1,19 @@ +package blockdev + +// Both checks return the public sentinel directly so the caller's fmt.Errorf +// wrap places the right ErrMisaligned/ErrOutOfBounds in the chain without +// having to map opaque internal errors. + +func checkAlignment(off int64, length int) error { + if off%BlockSize != 0 || int64(length)%BlockSize != 0 { + return ErrMisaligned + } + return nil +} + +func checkBounds(off int64, length int, deviceSize int64) error { + if off < 0 || length < 0 || off+int64(length) > deviceSize { + return ErrOutOfBounds + } + return nil +}