package store import ( "bytes" "context" "database/sql" "errors" "fmt" "io" "os" "path/filepath" "github.com/klauspost/compress/zstd" "github.com/rvbox/rvbox/internal/domain" ) // EventView is a validated persisted command event. Payload is the stored // event bytes (normally deterministic protobuf bytes); callers that need the // uncompressed form should use DecodeEventPayload. type EventView struct { IssueUUID domain.UUID EventSeq uint64 ObservedUnixNano int64 ReceiptUnixNano int64 EventType uint16 Stream uint16 Compression uint16 RawLength uint64 Payload []byte ImmutableSHA256 [32]byte } // ReadCommandEvents returns at most limit events after afterEventSeq in strict // sequence order. Segment references are revalidated at read time; a malformed // or replaced segment is an error rather than silently returning partial data. func (store *Store) ReadCommandEvents(ctx context.Context, issue domain.UUID, afterEventSeq uint64, limit uint32) ([]EventView, error) { if issue == (domain.UUID{}) || limit == 0 || limit > 1000 { return nil, errors.New("invalid command event read") } database, err := store.openDatabase() if err != nil { return nil, err } rows, err := database.QueryContext(ctx, `SELECT ce.event_seq, ce.observed_at, ce.server_receipt_time, ce.event_type, ce.compression, ce.raw_bytes, ce.payload, ce.segment_ordinal, ce.segment_record_offset, ce.segment_record_length, os.path, ce.immutable_sha256 FROM command_events ce LEFT JOIN output_segments os ON os.issue_uuid = ce.issue_uuid AND os.ordinal = ce.segment_ordinal WHERE ce.issue_uuid = ? AND ce.event_seq > ? ORDER BY ce.event_seq LIMIT ?`, issue[:], afterEventSeq, limit) if err != nil { return nil, err } defer rows.Close() result := make([]EventView, 0, limit) for rows.Next() { var event EventView var inline []byte var ordinal, offset, length sql.NullInt64 var path sql.NullString var digest []byte if err := rows.Scan(&event.EventSeq, &event.ObservedUnixNano, &event.ReceiptUnixNano, &event.EventType, &event.Compression, &event.RawLength, &inline, &ordinal, &offset, &length, &path, &digest); err != nil { return nil, err } if len(digest) != 32 || event.EventSeq == 0 || event.EventType == 0 { return nil, ErrInvalidSegmentRecord } copy(event.IssueUUID[:], issue[:]) copy(event.ImmutableSHA256[:], digest) switch { case inline != nil && !ordinal.Valid && !offset.Valid && !length.Valid && !path.Valid: event.Payload = append([]byte(nil), inline...) case inline == nil && ordinal.Valid && offset.Valid && length.Valid && path.Valid: if ordinal.Int64 < 0 || offset.Int64 < 0 || length.Int64 <= 0 || ordinal.Int64 > int64(^uint32(0)) || uint64(length.Int64) > uint64(^uint(0)>>1) { return nil, ErrInvalidSegmentRecord } payload, stream, readErr := store.readEventSegment(path.String, issue, event.EventSeq, uint32(ordinal.Int64), uint64(offset.Int64), uint64(length.Int64)) if readErr != nil { return nil, readErr } event.Payload = payload event.Stream = stream default: return nil, ErrInvalidSegmentRecord } if uint64(len(event.Payload)) > DefaultSegmentLimit || event.Compression < 1 || event.Compression > 2 || event.RawLength > DefaultSegmentLimit { return nil, ErrInvalidSegmentRecord } result = append(result, event) } if err := rows.Err(); err != nil { return nil, err } return result, nil } func (store *Store) readEventSegment(name string, issue domain.UUID, sequence uint64, ordinal uint32, offset, length uint64) ([]byte, uint16, error) { if filepath.Base(name) != name { return nil, 0, ErrUnsafeSegmentReference } owner, parsedOrdinal, err := parseSegmentName(name) if err != nil || owner != [16]byte(issue) || parsedOrdinal != ordinal { return nil, 0, ErrUnsafeSegmentReference } path := filepath.Join(store.segmentDirectory(), name) info, err := os.Lstat(path) if err != nil { return nil, 0, err } if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm()&0o077 != 0 { return nil, 0, ErrUnsafeSegmentReference } if length > uint64(^uint(0)>>1) || offset > uint64(^uint(0)>>1) || offset+length < offset || offset+length > uint64(info.Size()) { return nil, 0, ErrCommittedRangeMissing } file, err := os.Open(path) if err != nil { return nil, 0, err } defer file.Close() reader := io.NewSectionReader(file, int64(offset), int64(length)) record, encodedLength, err := DecodeSegmentRecord(reader, DefaultSegmentLimits()) if err != nil { return nil, 0, fmt.Errorf("decode command event segment: %w", err) } if encodedLength != length || record.OwnerUUID != [16]byte(issue) || record.Sequence != sequence || record.Kind != PayloadKindCommandEvent { return nil, 0, ErrInvalidSegmentRecord } return append([]byte(nil), record.Payload...), record.Stream, nil } // DecodeEventPayload validates and boundedly decompresses a stored event // payload. It is intentionally generic so output/follow handlers share the // same decompression ceiling. func DecodeEventPayload(event EventView) ([]byte, error) { if event.Compression == 1 { if event.RawLength != uint64(len(event.Payload)) { return nil, ErrInvalidSegmentRecord } return append([]byte(nil), event.Payload...), nil } if event.Compression != 2 || event.RawLength > DefaultSegmentLimit { return nil, ErrInvalidSegmentRecord } decoder, err := zstd.NewReader(bytes.NewReader(event.Payload), zstd.WithDecoderConcurrency(1), zstd.WithDecoderMaxMemory(DefaultSegmentLimit+1)) if err != nil { return nil, ErrInvalidSegmentRecord } defer decoder.Close() decoded, err := io.ReadAll(io.LimitReader(decoder, int64(event.RawLength)+1)) if err != nil || uint64(len(decoded)) != event.RawLength { return nil, ErrInvalidSegmentRecord } return decoded, nil }