package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( "fmt" "strings" ) // A state of the range parser. type rangeParserState int const ( rpsInit rangeParserState = iota // Initial state rpsExpectStart // Expect the start of the range rpsInStart // Reading the start of the range rpsExpectColon // Expect the colon that separates the start and end rpsExpectEnd // Expect the end of the range rpsInEnd // Reading the end of the range ) // An error emitted by the range parser. type rangeParserError struct { input string position int message string } // parseError creates a new range parser error. func parseError(input string, position int, message string) *rangeParserError { return &rangeParserError{ input: input, position: position, message: message, } } // Error implements the error interface for the range parser error type. func (rpe *rangeParserError) Error() string { return fmt.Sprintf("in `%s' at position %d: %s", rpe.input, rpe.position, rpe.message) } // The full state of the range parser. type rangeParser struct { runes []rune // The runes being parsed index int // The read position state rangeParserState // The FSM state startOfValue int // The position of the start of the value being read strBuilder strings.Builder // An accumulator for values output Range // The output being generated } // initRangeParser initializes the range parser from the specified input. func initRangeParser(input string) *rangeParser { return &rangeParser{ runes: []rune(input), state: rpsInit, } } // handleInit handles the parser's rpsInit state. It can accept a '@' or skip // to rpsExpectStart. func (rp *rangeParser) handleInit(current rune) { if current == '@' { rp.output.inside = true rp.index++ } rp.state = rpsExpectStart } // handleExpectStart handles the parser's rpsExpectStart state. It may accept // a colon, which will move the parser to the rpsExpectEnd state, a '~' which // will move it to rpsExpectColon, or any other rune which will cause the // accumulation of the "start" value to begin, and the state to be moved to // handleInStart. func (rp *rangeParser) handleExpectStart(current rune) { switch current { case ':': rp.output.start = "0" rp.state = rpsExpectEnd rp.index++ case '~': rp.state = rpsExpectColon rp.index++ default: rp.startOfValue = rp.index rp.state = rpsInStart } } // handleInStart handles the parser's rpsInStart state, which corresponds to the // reading of the "start" value. If a colon is found, the value will be written // to the range structure and validated, before switching to the rpsExpectEnd // state. Otherwise it will simply accumulate runes. func (rp *rangeParser) handleInStart(current rune) error { switch current { case ':': rp.output.start = rp.strBuilder.String() if !valueCheck.MatchString(rp.output.start) { return parseError(string(rp.runes), rp.startOfValue, "invalid start value") } rp.strBuilder.Reset() rp.state = rpsExpectEnd default: rp.strBuilder.WriteRune(current) } rp.index++ return nil } // handleExpectColon handles the parser's rpsExpectColon state. A colon MUST be // read, otherwise an error will be returned. The parser will then switch to the // rpsExpectEnd state. func (rp *rangeParser) handleExpectColon(current rune) error { switch current { case ':': rp.state = rpsExpectEnd rp.index++ default: return parseError(string(rp.runes), rp.index, "expected ':'") } return nil } // handleExpectEnd handles the parser's rpsExpectEnd state, which sets the // position of the beginning of the "end" value, and jumps to the rpsInEnd // state. func (rp *rangeParser) handleExpectEnd() { rp.startOfValue = rp.index rp.state = rpsInEnd } // handleInEnd handles the parser's rpsInEnd state, which accumulates runes for // the "end" value. func (rp *rangeParser) handleInEnd(current rune) { rp.strBuilder.WriteRune(current) rp.index++ } // consumeInput is the parser's state machine. func (rp *rangeParser) consumeInput() error { for rp.index < len(rp.runes) { var err error curRune := rp.runes[rp.index] switch rp.state { case rpsInit: rp.handleInit(curRune) case rpsExpectStart: rp.handleExpectStart(curRune) case rpsInStart: err = rp.handleInStart(curRune) case rpsExpectColon: err = rp.handleExpectColon(curRune) case rpsExpectEnd: rp.handleExpectEnd() case rpsInEnd: rp.handleInEnd(curRune) } if err != nil { return err } } return nil } // Try to parse a string into a performance data range. func ParseRange(input string) (*Range, error) { parser := initRangeParser(input) if err := parser.consumeInput(); err != nil { return nil, err } if parser.state == rpsInStart { // The range was a single value, so that's the upper bound. parser.output.start = "0" parser.state = rpsInEnd } if parser.state != rpsInEnd { return nil, parseError(input, parser.index, "unexpected end of input") } parser.output.end = parser.strBuilder.String() if !valueCheck.MatchString(parser.output.end) { return nil, parseError(input, parser.startOfValue, "invalid end value") } return &parser.output, nil }