package wal import ( "encoding/binary" "errors" "hash/crc32" ) // PhysicalRecord represents a single physical record in the WAL. type PhysicalRecord struct { CRC uint32 Length uint16 Type uint8 Payload []byte } // EncodePhysicalRecord encodes a physical record with the given type and payload. // Format: [crc32 u32 LE][length u16 LE][type u8][payload bytes] // CRC covers length + type + payload. func EncodePhysicalRecord(recType uint8, payload []byte) []byte { length := uint16(len(payload)) buf := make([]byte, PhysicalRecordHeaderSize+len(payload)) // Write length and type first so we can compute CRC. binary.LittleEndian.PutUint16(buf[4:6], length) buf[6] = recType copy(buf[7:], payload) // CRC covers bytes [4:] = length + type + payload. crc := crc32.ChecksumIEEE(buf[4:]) binary.LittleEndian.PutUint32(buf[0:4], crc) return buf } // DecodePhysicalRecord decodes a physical record from data. // Returns the record, number of bytes consumed, and any error. func DecodePhysicalRecord(data []byte) (rec *PhysicalRecord, consumed int, err error) { if len(data) < PhysicalRecordHeaderSize { return nil, 0, errors.New("record: data too short for header") } crc := binary.LittleEndian.Uint32(data[0:4]) length := binary.LittleEndian.Uint16(data[4:6]) recType := data[6] if int(length) > len(data)-PhysicalRecordHeaderSize { return nil, 0, errors.New("record: data too short for payload") } payload := make([]byte, length) copy(payload, data[7:7+length]) // Verify CRC: covers length + type + payload. expectedCRC := crc32.ChecksumIEEE(data[4 : 7+length]) if crc != expectedCRC { return nil, 0, errors.New("record: CRC mismatch") } consumed = PhysicalRecordHeaderSize + int(length) return &PhysicalRecord{ CRC: crc, Length: length, Type: recType, Payload: payload, }, consumed, nil } // PaddingNeeded returns the number of padding bytes needed at blockOffset. // If the remaining space in the current block is <= PhysicalRecordHeaderSize (7), // that remaining space must be zero-padded. func PaddingNeeded(blockOffset uint32) int { remaining := WalBlockSize - (blockOffset % WalBlockSize) if remaining <= PhysicalRecordHeaderSize { return int(remaining) } return 0 } // CanFitRecord reports whether a physical record with the given payload length // can fit in the current block starting at blockOffset. func CanFitRecord(blockOffset uint32, payloadLen uint32) bool { remaining := WalBlockSize - (blockOffset % WalBlockSize) return int(remaining) >= PhysicalRecordHeaderSize+int(payloadLen) } // SplitIntoRecords splits an encoded WAL batch into physical record payloads // respecting 32 KB block boundaries. // Each returned byte slice is the full encoded physical record (header + payload). func SplitIntoRecords(encodedBatch []byte) [][]byte { maxPayload := WalBlockSize - PhysicalRecordHeaderSize total := len(encodedBatch) if total == 0 { return nil } // Single record fits entirely. if total <= maxPayload { return [][]byte{EncodePhysicalRecord(RecFull, encodedBatch)} } var records [][]byte offset := 0 for offset < total { chunkLen := min(total-offset, maxPayload) var recType uint8 switch { case offset == 0 && offset+chunkLen == total: recType = RecFull case offset == 0: recType = RecFirst case offset+chunkLen == total: recType = RecLast default: recType = RecMiddle } records = append(records, EncodePhysicalRecord(recType, encodedBatch[offset:offset+chunkLen])) offset += chunkLen } return records }