Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 20 additions & 23 deletions blockdev.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 (
Expand All @@ -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
}

Expand Down Expand Up @@ -74,21 +71,21 @@ 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
}
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])
Expand Down Expand Up @@ -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
}
Expand Down
48 changes: 0 additions & 48 deletions internal/overlay/overlay.go

This file was deleted.

27 changes: 0 additions & 27 deletions internal/validate/validate.go

This file was deleted.

48 changes: 48 additions & 0 deletions overlay.go
Original file line number Diff line number Diff line change
@@ -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)
}
19 changes: 19 additions & 0 deletions validate.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading