package wal import ( "bytes" "encoding/binary" "errors" "fmt" ) // WalEntry represents a single WAL record. type WalEntry struct { OpType uint8 ValueKind uint8 Key []byte Value []byte } // Validate checks that the entry fields are consistent with the design rules. func (e *WalEntry) Validate() error { keyLen := len(e.Key) if keyLen == 0 || keyLen > int(MaxWalKeyBytes) { return fmt.Errorf("wal: invalid key length %d", keyLen) } valLen := len(e.Value) switch e.OpType { case OpPut: switch e.ValueKind { case VKInline: if valLen > int(MaxWalInlineValueBytes) { return fmt.Errorf("wal: inline value length %d out of range [0, %d]", valLen, MaxWalInlineValueBytes) } case VKValueLogPointer: if valLen == 0 { return errors.New("wal: value log pointer requires non-empty value") } default: return fmt.Errorf("wal: put requires valueKind Inline(1) or ValueLogPointer(2), got %d", e.ValueKind) } case OpDelete: if e.ValueKind != VKNone { return fmt.Errorf("wal: delete requires valueKind None(0), got %d", e.ValueKind) } if valLen != 0 { return fmt.Errorf("wal: delete requires empty value, got length %d", valLen) } default: return fmt.Errorf("wal: invalid opType %d", e.OpType) } return nil } // EncodeEntry serializes a WalEntry into a byte slice. func EncodeEntry(e *WalEntry) ([]byte, error) { if err := e.Validate(); err != nil { return nil, err } keyLen := uint64(len(e.Key)) valLen := uint64(len(e.Value)) // Size: 1 (opType) + 1 (valueKind) + varint(keyLen) + varint(valLen) + key + value size := 2 + MaxWalVarintBytes + MaxWalVarintBytes + len(e.Key) + len(e.Value) buf := make([]byte, size) buf[0] = e.OpType buf[1] = e.ValueKind n := 2 n += binary.PutUvarint(buf[n:], keyLen) n += binary.PutUvarint(buf[n:], valLen) n += copy(buf[n:], e.Key) n += copy(buf[n:], e.Value) return buf[:n], nil } // DecodeEntry deserializes a WalEntry from a byte slice. // Returns the decoded entry and the number of bytes consumed. func DecodeEntry(data []byte) (entry *WalEntry, consumed int, err error) { if len(data) < 2 { return nil, 0, errors.New("wal: data too short for entry header") } opType := data[0] valueKind := data[1] r := bytes.NewReader(data[2:]) keyLen, err := binary.ReadUvarint(r) if err != nil { return nil, 0, fmt.Errorf("wal: reading key length: %w", err) } valLen, err := binary.ReadUvarint(r) if err != nil { return nil, 0, fmt.Errorf("wal: reading value length: %w", err) } // Calculate consumed so far: 2 header bytes + bytes read from reader consumed = 2 + (len(data) - 2 - r.Len()) // Read key remaining := len(data) - consumed if uint64(remaining) < keyLen { return nil, 0, fmt.Errorf("wal: data truncated: need %d bytes for key, have %d", keyLen, remaining) } key := make([]byte, keyLen) copy(key, data[consumed:consumed+int(keyLen)]) consumed += int(keyLen) // Read value remaining = len(data) - consumed if uint64(remaining) < valLen { return nil, 0, fmt.Errorf("wal: data truncated: need %d bytes for value, have %d", valLen, remaining) } value := make([]byte, valLen) copy(value, data[consumed:consumed+int(valLen)]) consumed += int(valLen) e := &WalEntry{ OpType: opType, ValueKind: valueKind, Key: key, Value: value, } if err := e.Validate(); err != nil { return nil, 0, err } return e, consumed, nil }