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
58 changes: 53 additions & 5 deletions blockdev.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,57 @@
package blockdev

// BlockSize is the unit of read, write, and tracking in bytes. All offsets
// and lengths passed to ReadAt and WriteAt must be multiples of BlockSize.
import (
"fmt"
"io"

"github.com/codingminions/blockdev/internal/validate"
)

// BlockSize is 4096 to match the OS page size and NBD's expected sector size.
// Anything smaller would cost read-modify-write; anything larger would inflate
// the overlay's per-changed-block storage.
const BlockSize = 4096

// BlockDevice is an in-memory block device with copy-on-write semantics.
// Construct one with New. Safe for concurrent use.
type BlockDevice struct{}
// BlockDevice retains the base without copying. Caller must not mutate base
// after handoff; defensive copy would double peak memory when many sandboxes
// share one image.
type BlockDevice struct {
base []byte
length int64
}

var _ io.ReaderAt = (*BlockDevice)(nil)

// New validates alignment up front rather than at first ReadAt — a misaligned
// base is a configuration bug, and we'd rather fail at construction than after
// a sandbox has been wired up.
func New(initial []byte) (*BlockDevice, error) {
if err := validate.Alignment(0, len(initial)); err != nil {
return nil, fmt.Errorf("blockdev.New: len(initial)=%d: %w", len(initial), ErrMisaligned)
}
return &BlockDevice{
base: initial,
length: int64(len(initial)),
}, nil
}

// ReadAt validates bounds before alignment so negative offsets surface as
// ErrOutOfBounds (a range bug) rather than ErrMisaligned (a shape bug); the
// distinction matters for log greppability when NBD pipelines requests.
// On any failure returns (0, err) and leaves p untouched — partial reads
// could corrupt the guest filesystem before the kernel notices.
func (b *BlockDevice) ReadAt(p []byte, off int64) (int, error) {
if err := validate.Bounds(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)
}
if err := validate.Alignment(off, len(p)); err != nil {
return 0, fmt.Errorf("blockdev.ReadAt off=%d len=%d: %w",
off, len(p), ErrMisaligned)
}
if len(p) == 0 {
return 0, nil
}
copy(p, b.base[off:off+int64(len(p))])
return len(p), nil
}
23 changes: 4 additions & 19 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,7 @@
// Package blockdev implements an in-memory block device backend with
// copy-on-write semantics.
//
// A BlockDevice presents a fixed-size byte array as a sequence of 4096-byte
// blocks. Writes are captured in an in-memory overlay layered above an
// immutable base. Reads consult the overlay first, falling through to the
// base when a block has not been written.
//
// Serialize produces a compact byte representation of just the overlay;
// Deserialize reconstructs a BlockDevice from a serialized overlay plus the
// original base. This makes blockdev a natural fit for sandbox snapshot and
// resume systems where many sandboxes share an identical base filesystem
// image and storing per-sandbox diffs is preferred over storing per-sandbox
// copies.
//
// Block size is fixed at 4096 bytes (see BlockSize). Offsets and lengths
// passed to ReadAt and WriteAt must be multiples of BlockSize. The initial
// base passed to New must be a multiple of BlockSize and must not be mutated
// by the caller after construction.
// Package blockdev is an in-memory copy-on-write block device. Many sandboxes
// share one read-only base in RAM while each captures its own writes in an
// overlay. Serialize emits only the overlay so per-sandbox snapshots stay
// small, regardless of base size.
//
// All public methods are safe for concurrent use.
package blockdev
27 changes: 27 additions & 0 deletions internal/validate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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
}
Loading