From e808ddca774aae51b1dc13b6f59e2ada836767b8 Mon Sep 17 00:00:00 2001 From: Prateek Kumar Date: Mon, 18 May 2026 01:34:30 -0700 Subject: [PATCH] add the initial read path workflow. --- blockdev.go | 58 ++++++++++++++++++++++++++++++++--- doc.go | 23 +++----------- errors.go | 11 +++++++ internal/validate/validate.go | 27 ++++++++++++++++ 4 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 errors.go create mode 100644 internal/validate/validate.go diff --git a/blockdev.go b/blockdev.go index e8c6c01..2289c80 100644 --- a/blockdev.go +++ b/blockdev.go @@ -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 +} diff --git a/doc.go b/doc.go index 6854d17..a729982 100644 --- a/doc.go +++ b/doc.go @@ -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 diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..cc0aa41 --- /dev/null +++ b/errors.go @@ -0,0 +1,11 @@ +package blockdev + +import "errors" + +// Sentinels for errors.Is matching; methods wrap them with operation context +// so callers branch on category, not on parsed message strings. +var ( + ErrMisaligned = errors.New("blockdev: offset or length not block-aligned") + ErrOutOfBounds = errors.New("blockdev: read/write beyond device length") + ErrBadFormat = errors.New("blockdev: malformed serialized data") +) diff --git a/internal/validate/validate.go b/internal/validate/validate.go new file mode 100644 index 0000000..453073a --- /dev/null +++ b/internal/validate/validate.go @@ -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 +}