package wal import ( "bytes" "errors" "fmt" "os" ) // TailCorruptionError indicates that the WAL tail contains corrupt data // (bad CRC, unexpected non-zero padding bytes, etc.). Recovery may safely // truncate at the last valid record. type TailCorruptionError struct { Offset int Err error } func (e *TailCorruptionError) Error() string { return fmt.Sprintf("wal: tail corruption at offset %d: %v", e.Offset, e.Err) } func (e *TailCorruptionError) Unwrap() error { return e.Err } // IsTailCorruption reports whether err indicates tail corruption in a WAL // segment. Callers may safely truncate the segment at the last valid record. func IsTailCorruption(err error) bool { var tce *TailCorruptionError return errors.As(err, &tce) } // ParseBlock parses physical records from a single block of raw bytes. // The block is typically WalBlockSize (32 KB) bytes, but the last block of a // segment may be shorter. Trailing bytes after the last record must be all // zeros (padding); non-zero trailing bytes produce a TailCorruptionError. func ParseBlock(data []byte) ([]*PhysicalRecord, error) { var records []*PhysicalRecord pos := 0 for pos < len(data) { remaining := len(data) - pos // If fewer than PhysicalRecordHeaderSize bytes remain, they must be // zero-padding. if remaining < PhysicalRecordHeaderSize { tail := data[pos:] if !isAllZeros(tail) { return records[:len(records):len(records)], &TailCorruptionError{ Offset: pos, Err: fmt.Errorf("non-zero padding bytes in tail (%d bytes)", len(tail)), } } break } // Check for zero-filled header (preallocated / unwritten space). if isAllZeros(data[pos : pos+PhysicalRecordHeaderSize]) { // Verify rest of block is also zeros. if !isAllZeros(data[pos:]) { return records[:len(records):len(records)], &TailCorruptionError{ Offset: pos, Err: errors.New("zero header but non-zero bytes follow"), } } break } rec, consumed, err := DecodePhysicalRecord(data[pos:]) if err != nil { return records[:len(records):len(records)], &TailCorruptionError{ Offset: pos, Err: err, } } records = append(records, rec) pos += consumed } return records, nil } // ParseRecordsFromFile opens a WAL segment file, skips the file header, reads // blocks sequentially, and returns all physical records in order. Short final // blocks are handled correctly. func ParseRecordsFromFile(filePath string) ([]*PhysicalRecord, error) { f, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("wal: parse records: %w", err) } defer f.Close() // Skip file header. if _, err := f.Seek(WalFileHeaderSize, 0); err != nil { return nil, fmt.Errorf("wal: seek past header: %w", err) } var allRecords []*PhysicalRecord buf := make([]byte, WalBlockSize) for { n, readErr := f.Read(buf) if readErr != nil { if errors.Is(readErr, os.ErrClosed) { return nil, fmt.Errorf("wal: file closed during read: %w", readErr) } break } if n == 0 { break } blockData := buf[:n] recs, err := ParseBlock(blockData) if err != nil { // Return records collected so far along with the error. return allRecords, err } allRecords = append(allRecords, recs...) // If we got a short block, this was the last one. if n < WalBlockSize { break } } return allRecords, nil } func isAllZeros(data []byte) bool { return bytes.Count(data, []byte{0}) == len(data) }