154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
package fs
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"ollie/format"
|
|
)
|
|
|
|
// block tracks a single open [[[block]]] for nesting validation.
|
|
type block struct {
|
|
header format.BlockHeader
|
|
line int // 1-indexed line number where this block opened
|
|
}
|
|
|
|
// LogValidator enforces mechanical guarantees on the [[[block]]]/[[[end]]] protocol:
|
|
// - Every [[[block]]] MUST have an [[[end]]] marker
|
|
// - Blocks SHALL NOT be nested
|
|
//
|
|
// It operates as a streaming state machine, buffering partial lines across calls
|
|
// to Validate() so that block headers split across writes are handled correctly.
|
|
type LogValidator struct {
|
|
mu sync.Mutex
|
|
stack []block // stack of currently open blocks (should never exceed depth 1)
|
|
buf string // incomplete last line accumulated across calls
|
|
errs []violation // accumulated validation errors
|
|
fatal bool // once set, all subsequent data is rejected
|
|
lineNum int // 1-indexed line counter within buf
|
|
}
|
|
|
|
// violation records a single structural protocol violation.
|
|
type violation struct {
|
|
line int
|
|
msg string
|
|
}
|
|
|
|
func (v violation) Error() string {
|
|
return fmt.Sprintf("log structure violation at line %d: %s", v.line, v.msg)
|
|
}
|
|
|
|
// NewLogValidator creates a fresh validator with no open blocks.
|
|
func NewLogValidator() *LogValidator {
|
|
return &LogValidator{}
|
|
}
|
|
|
|
// Validate processes a chunk of log data through the state machine.
|
|
// It buffers incomplete lines across calls so that block markers split
|
|
// across writes are handled correctly. Returns nil if valid, or a slice
|
|
// of violations if any structural rules are broken.
|
|
//
|
|
// The state machine recognizes two kinds of lines:
|
|
// - Block headers: lines matching "[[[<role>]]]" or "[[[<role>:<name>]]]"
|
|
// - End markers: the exact line "[[[end]]]"
|
|
//
|
|
// Rules enforced:
|
|
// 1. A block header while another block is open → "nested block" error
|
|
// 2. An end marker with no open block → "unexpected end" error
|
|
// 3. At session end, unclosed blocks are reported via UnclosedBlocks()
|
|
func (v *LogValidator) Validate(data []byte) []error {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
if v.fatal {
|
|
return []error{fmt.Errorf("validator already failed — rejecting further data")}
|
|
}
|
|
|
|
v.buf += string(data)
|
|
|
|
lines := strings.Split(v.buf, "\n")
|
|
v.buf = lines[len(lines)-1] // keep incomplete trailing content
|
|
|
|
for i := 0; i < len(lines)-1; i++ {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line == "" {
|
|
continue
|
|
}
|
|
v.lineNum++
|
|
|
|
if format.IsEndMarker(line) {
|
|
v.handleEndMarker(line)
|
|
} else if bh := format.ParseBlockHeader(line); bh != nil {
|
|
v.handleBlockHeader(bh, line)
|
|
}
|
|
// Lines that are neither block headers nor end markers are ignored
|
|
// (they're block body content).
|
|
}
|
|
|
|
if v.fatal {
|
|
errs := make([]error, len(v.errs))
|
|
for i, e := range v.errs {
|
|
errs[i] = e
|
|
}
|
|
return errs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *LogValidator) handleEndMarker(line string) {
|
|
if len(v.stack) == 0 {
|
|
v.errs = append(v.errs, violation{
|
|
line: v.lineNum,
|
|
msg: fmt.Sprintf("unexpected [%s] with no open block", line),
|
|
})
|
|
v.fatal = true
|
|
return
|
|
}
|
|
top := v.stack[len(v.stack)-1]
|
|
v.stack = v.stack[:len(v.stack)-1]
|
|
_ = top // consumed
|
|
}
|
|
|
|
func (v *LogValidator) handleBlockHeader(bh *format.BlockHeader, line string) {
|
|
if len(v.stack) > 0 {
|
|
outer := v.stack[len(v.stack)-1]
|
|
v.errs = append(v.errs, violation{
|
|
line: v.lineNum,
|
|
msg: fmt.Sprintf("nested block [%s] while [%s:%s] is still open (no nesting allowed)",
|
|
line, outer.header.Role, outer.header.Name),
|
|
})
|
|
v.fatal = true
|
|
return
|
|
}
|
|
v.stack = append(v.stack, block{header: *bh, line: v.lineNum})
|
|
}
|
|
|
|
// UnclosedBlocks returns a list of blocks that were opened but never closed.
|
|
// Call this at session/agent teardown to detect missing [[[end]]] markers.
|
|
func (v *LogValidator) UnclosedBlocks() []block {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
out := make([]block, len(v.stack))
|
|
copy(out, v.stack)
|
|
return out
|
|
}
|
|
|
|
// HasViolations reports whether any violations have been recorded.
|
|
func (v *LogValidator) HasViolations() bool {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return v.fatal || len(v.errs) > 0
|
|
}
|
|
|
|
// Errors returns all accumulated violations (non-nil only after fatal failure).
|
|
func (v *LogValidator) Errors() []error {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
out := make([]error, len(v.errs))
|
|
for i, e := range v.errs {
|
|
out[i] = e
|
|
}
|
|
return out
|
|
}
|