package blockvol import ( "encoding/binary" "errors" "fmt" "io" "github.com/google/uuid" ) const ( SuperblockSize = 4096 MagicSWBK = "SWBK" CurrentVersion = 1 ) var ( ErrNotBlockVol = errors.New("blockvol: not a blockvol file (bad magic)") ErrUnsupportedVersion = errors.New("blockvol: unsupported version") ErrInvalidVolumeSize = errors.New("blockvol: volume size must be > 0") ) // Superblock is the 4KB header at offset 0 of a blockvol file. // It identifies the file format, stores volume geometry, and tracks WAL state. type Superblock struct { Magic [4]byte Version uint16 Flags uint16 UUID [16]byte VolumeSize uint64 // logical size in bytes ExtentSize uint32 // default 64KB BlockSize uint32 // default 4KB (min I/O unit) WALOffset uint64 // byte offset where WAL region starts WALSize uint64 // WAL region size in bytes WALHead uint64 // logical WAL write position (monotonically increasing) WALTail uint64 // logical WAL flush position (monotonically increasing) WALCheckpointLSN uint64 // last LSN flushed to extent region Replication [4]byte CreatedAt uint64 // unix timestamp SnapshotCount uint32 Epoch uint64 // fencing epoch (0 = no fencing, Phase 3 compat) DurabilityMode uint8 // CP8-3-1: 0=best_effort, 1=sync_all, 2=sync_quorum StorageProfile uint8 // CP11A-1: 0=single, 1=striped (reserved) PreparedSize uint64 // CP11A-2: pending expand size (0 = no expand in flight) ExpandEpoch uint64 // CP11A-2: expand operation ID (0 = none) } // superblockOnDisk is the fixed-size on-disk layout (binary.Write/Read target). // Must be <= SuperblockSize bytes. Remaining space is zero-padded. type superblockOnDisk struct { Magic [4]byte Version uint16 Flags uint16 UUID [16]byte VolumeSize uint64 ExtentSize uint32 BlockSize uint32 WALOffset uint64 WALSize uint64 WALHead uint64 WALTail uint64 WALCheckpointLSN uint64 Replication [4]byte CreatedAt uint64 SnapshotCount uint32 Epoch uint64 DurabilityMode uint8 StorageProfile uint8 PreparedSize uint64 ExpandEpoch uint64 } // NewSuperblock creates a superblock with defaults and a fresh UUID. func NewSuperblock(volumeSize uint64, opts CreateOptions) (Superblock, error) { if volumeSize == 0 { return Superblock{}, ErrInvalidVolumeSize } extentSize := opts.ExtentSize if extentSize == 0 { extentSize = 64 * 1024 // 64KB } blockSize := opts.BlockSize if blockSize == 0 { blockSize = 4096 // 4KB } walSize := opts.WALSize if walSize == 0 { walSize = 64 * 1024 * 1024 // 64MB } var repl [4]byte if opts.Replication != "" { copy(repl[:], opts.Replication) } else { copy(repl[:], "000") } id := uuid.New() sb := Superblock{ Version: CurrentVersion, VolumeSize: volumeSize, ExtentSize: extentSize, BlockSize: blockSize, WALOffset: SuperblockSize, WALSize: walSize, DurabilityMode: uint8(opts.DurabilityMode), StorageProfile: uint8(opts.StorageProfile), } copy(sb.Magic[:], MagicSWBK) sb.UUID = id sb.Replication = repl return sb, nil } // WriteTo serializes the superblock to w as a 4096-byte block. func (sb *Superblock) WriteTo(w io.Writer) (int64, error) { buf := make([]byte, SuperblockSize) d := superblockOnDisk{ Magic: sb.Magic, Version: sb.Version, Flags: sb.Flags, UUID: sb.UUID, VolumeSize: sb.VolumeSize, ExtentSize: sb.ExtentSize, BlockSize: sb.BlockSize, WALOffset: sb.WALOffset, WALSize: sb.WALSize, WALHead: sb.WALHead, WALTail: sb.WALTail, WALCheckpointLSN: sb.WALCheckpointLSN, Replication: sb.Replication, CreatedAt: sb.CreatedAt, SnapshotCount: sb.SnapshotCount, Epoch: sb.Epoch, DurabilityMode: sb.DurabilityMode, StorageProfile: sb.StorageProfile, PreparedSize: sb.PreparedSize, ExpandEpoch: sb.ExpandEpoch, } // Encode into beginning of buf; rest stays zero (padding). endian := binary.LittleEndian off := 0 off += copy(buf[off:], d.Magic[:]) endian.PutUint16(buf[off:], d.Version) off += 2 endian.PutUint16(buf[off:], d.Flags) off += 2 off += copy(buf[off:], d.UUID[:]) endian.PutUint64(buf[off:], d.VolumeSize) off += 8 endian.PutUint32(buf[off:], d.ExtentSize) off += 4 endian.PutUint32(buf[off:], d.BlockSize) off += 4 endian.PutUint64(buf[off:], d.WALOffset) off += 8 endian.PutUint64(buf[off:], d.WALSize) off += 8 endian.PutUint64(buf[off:], d.WALHead) off += 8 endian.PutUint64(buf[off:], d.WALTail) off += 8 endian.PutUint64(buf[off:], d.WALCheckpointLSN) off += 8 off += copy(buf[off:], d.Replication[:]) endian.PutUint64(buf[off:], d.CreatedAt) off += 8 endian.PutUint32(buf[off:], d.SnapshotCount) off += 4 endian.PutUint64(buf[off:], d.Epoch) off += 8 buf[off] = d.DurabilityMode off++ buf[off] = d.StorageProfile off++ endian.PutUint64(buf[off:], d.PreparedSize) off += 8 endian.PutUint64(buf[off:], d.ExpandEpoch) n, err := w.Write(buf) return int64(n), err } // ReadSuperblock reads and validates a superblock from r. func ReadSuperblock(r io.Reader) (Superblock, error) { buf := make([]byte, SuperblockSize) if _, err := io.ReadFull(r, buf); err != nil { return Superblock{}, fmt.Errorf("blockvol: read superblock: %w", err) } endian := binary.LittleEndian var sb Superblock off := 0 copy(sb.Magic[:], buf[off:off+4]) off += 4 if string(sb.Magic[:]) != MagicSWBK { return Superblock{}, ErrNotBlockVol } sb.Version = endian.Uint16(buf[off:]) off += 2 if sb.Version != CurrentVersion { return Superblock{}, fmt.Errorf("%w: got %d, want %d", ErrUnsupportedVersion, sb.Version, CurrentVersion) } sb.Flags = endian.Uint16(buf[off:]) off += 2 copy(sb.UUID[:], buf[off:off+16]) off += 16 sb.VolumeSize = endian.Uint64(buf[off:]) off += 8 if sb.VolumeSize == 0 { return Superblock{}, ErrInvalidVolumeSize } sb.ExtentSize = endian.Uint32(buf[off:]) off += 4 sb.BlockSize = endian.Uint32(buf[off:]) off += 4 sb.WALOffset = endian.Uint64(buf[off:]) off += 8 sb.WALSize = endian.Uint64(buf[off:]) off += 8 sb.WALHead = endian.Uint64(buf[off:]) off += 8 sb.WALTail = endian.Uint64(buf[off:]) off += 8 sb.WALCheckpointLSN = endian.Uint64(buf[off:]) off += 8 copy(sb.Replication[:], buf[off:off+4]) off += 4 sb.CreatedAt = endian.Uint64(buf[off:]) off += 8 sb.SnapshotCount = endian.Uint32(buf[off:]) off += 4 sb.Epoch = endian.Uint64(buf[off:]) off += 8 sb.DurabilityMode = buf[off] off++ sb.StorageProfile = buf[off] off++ sb.PreparedSize = endian.Uint64(buf[off:]) off += 8 sb.ExpandEpoch = endian.Uint64(buf[off:]) return sb, nil } var ErrInvalidSuperblock = errors.New("blockvol: invalid superblock") // Validate checks that the superblock fields are internally consistent. func (sb *Superblock) Validate() error { if string(sb.Magic[:]) != MagicSWBK { return ErrNotBlockVol } if sb.Version != CurrentVersion { return fmt.Errorf("%w: got %d", ErrUnsupportedVersion, sb.Version) } if sb.VolumeSize == 0 { return ErrInvalidVolumeSize } if sb.BlockSize == 0 { return fmt.Errorf("%w: BlockSize is 0", ErrInvalidSuperblock) } if sb.ExtentSize == 0 { return fmt.Errorf("%w: ExtentSize is 0", ErrInvalidSuperblock) } if sb.WALSize == 0 { return fmt.Errorf("%w: WALSize is 0", ErrInvalidSuperblock) } if sb.WALOffset != SuperblockSize { return fmt.Errorf("%w: WALOffset=%d, expected %d", ErrInvalidSuperblock, sb.WALOffset, SuperblockSize) } if sb.VolumeSize%uint64(sb.BlockSize) != 0 { return fmt.Errorf("%w: VolumeSize %d not aligned to BlockSize %d", ErrInvalidSuperblock, sb.VolumeSize, sb.BlockSize) } if sb.DurabilityMode > 2 { return fmt.Errorf("%w: invalid DurabilityMode %d", ErrInvalidSuperblock, sb.DurabilityMode) } if sb.StorageProfile > 1 { return fmt.Errorf("%w: invalid StorageProfile %d", ErrInvalidSuperblock, sb.StorageProfile) } if sb.PreparedSize != 0 && sb.PreparedSize <= sb.VolumeSize { return fmt.Errorf("%w: PreparedSize %d must be > VolumeSize %d", ErrInvalidSuperblock, sb.PreparedSize, sb.VolumeSize) } if sb.PreparedSize == 0 && sb.ExpandEpoch != 0 { return fmt.Errorf("%w: ExpandEpoch %d must be 0 when PreparedSize is 0", ErrInvalidSuperblock, sb.ExpandEpoch) } return nil }