diff --git a/internal/logparsers/parsers.go b/internal/logparsers/parsers.go index fe60f86..459f890 100644 --- a/internal/logparsers/parsers.go +++ b/internal/logparsers/parsers.go @@ -30,57 +30,66 @@ func (e *FormatError) Unwrap() error { return e.err } +// NewFormatError constructs a new format error. func NewFormatError(msg string, err error) error { return &FormatError{msg: msg, err: err} } - // type LineParserFunc is a function that takes a string // and returns a can frame. This is useful for common // can dump formats. type LineParserFunc func(string) (can.Frame, time.Time, error) -var candumpRegex = regexp.MustCompile(`^\((\d+)\.(\d{6}) \w+ (\w+)#(\w+)$`) +var candumpRegex = regexp.MustCompile(`^\((\d+)\.(\d{6})\) \w+ (\w+)#(\w+)$`) func parseCanDumpLine(dumpLine string) (frame can.Frame, ts time.Time, err error) { frame = can.Frame{} ts = time.Unix(0,0) // dumpline looks like this: // (1684538768.521889) can0 200#8D643546 - // remove trailing newline + // remove trailing newline/whitespaces dumpLine = strings.TrimSpace(dumpLine) - segments := strings.Split(dumpLine, " ") - if len(segments) != 3 { - err = NewFormatError("failed to split line", err) + m := candumpRegex.FindStringSubmatch(dumpLine) + if m == nil || len(m) != 5 { + err = NewFormatError("no regex match", nil) return } var unixSeconds, unixMicros int64 - fmt.Sscanf(segments[0], "(%d.%d)", &unixSeconds, &unixMicros) - // now we extract the remaining data: - hexes := strings.Split(segments[2], "#") // first portion is id, second is data + unixSeconds, err = strconv.ParseInt(m[1], 10, 0) + if err != nil { + err = NewFormatError("failed to parse unix seconds", err) + return + } + unixMicros, err = strconv.ParseInt(m[2], 10, 0) + if err != nil { + err = NewFormatError("failed to parse unix micros", err) + return + } - id, err := strconv.ParseUint(hexes[0], 16, 64) + id, err := strconv.ParseUint(m[3], 16, 64) if err != nil { err = NewFormatError("failed to parse id", err) return } - if (len(hexes[1]) % 2) != 0 { + if (len(m[4]) % 2) != 0 { err = NewFormatError("odd number of hex characters", nil) return } - rawData, err := hex.DecodeString(hexes[1]) + rawData, err := hex.DecodeString(m[4]) if err != nil { err = NewFormatError("failed to decode hex data", err) return } + // TODO: add extended id support, need an example log and a test. frame.Id = can.CanID{Id: uint32(id), Extended: false} frame.Data = rawData frame.Kind = can.CanDataFrame - ts = time.Unix(unixSeconds, unixMicros) + ts = time.Unix(unixSeconds, unixMicros * int64(time.Microsecond)) + return } @@ -156,10 +165,11 @@ func parseTelemLogLine(line string) (frame can.Frame, ts time.Time, err error) { } -// this is how we adapt a can frame source into one that produces -// skylab busevents +// BusParserFunc is a function that takes a string and returns a busevent. type BusParserFunc func(string) (skylab.BusEvent, error) +// parserBusEventMapper takes a line parser (that returns a can frame) +// and makes it return a busEvent instead. func parserBusEventMapper(f LineParserFunc) BusParserFunc { return func(s string) (skylab.BusEvent, error) { var b = skylab.BusEvent{} diff --git a/internal/logparsers/parsers_test.go b/internal/logparsers/parsers_test.go index bd0b0e3..529cdfa 100644 --- a/internal/logparsers/parsers_test.go +++ b/internal/logparsers/parsers_test.go @@ -19,13 +19,6 @@ func Test_parseCanDumpLine(t *testing.T) { wantTs time.Time wantErr bool }{ - { - name: "test garbage", - args: args{dumpLine: "hosireoie"}, - wantFrame: can.Frame{}, - wantTs: time.Unix(0,0), - wantErr: true, - }, { name: "test normal data", args: args{dumpLine: "(1684538768.521889) can0 200#8D643546"}, @@ -34,40 +27,16 @@ func Test_parseCanDumpLine(t *testing.T) { Data: []byte{0x8d, 0x64, 0x35, 0x46}, Kind: can.CanDataFrame, }, - wantTs: time.Unix(1684538768, 521889), + wantTs: time.Unix(1684538768, 521889 * int64(time.Microsecond)), wantErr: false, }, - { - name: "bad data length", - // odd number of hex data nibbles - args: args{dumpLine: "(1684538768.521889) can0 200#8D64354"}, - wantFrame: can.Frame{}, - wantTs: time.Unix(0,0), - wantErr: true, - }, - { - name: "invalid hex", - // J is not valid hex. - args: args{dumpLine: "(1684538768.521889) can0 200#8D64354J"}, - wantFrame: can.Frame{}, - wantTs: time.Unix(0,0), - wantErr: true, - }, - { - name: "bad time", - // we destroy the time structure. - args: args{dumpLine: "(badtime.521889) can0 200#8D643546"}, - wantFrame: can.Frame{}, - wantTs: time.Unix(0,0), - wantErr: true, - }, // TODO: add extended id test case } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotFrame, gotTs, err := parseCanDumpLine(tt.args.dumpLine) - if (err != nil) != tt.wantErr { + if (err == nil) == tt.wantErr { t.Errorf("parseCanDumpLine() error = %v, wantErr %v", err, tt.wantErr) return } @@ -80,3 +49,47 @@ func Test_parseCanDumpLine(t *testing.T) { }) } } + +func Test_parseCanDumpLine_errors(t *testing.T) { + // this test tries a bunch of failure cases to ensure that they are caught and not panicking. + + tests := []struct { + name string + input string + }{ + { + name: "garbage input", + input: "hoiseorhijkl", + }, + { + name: "bad data length", + // odd number of hex data nibbles + input: "(1684538768.521889) can0 200#8D64354", + }, + { + name: "invalid hex", + // J is not valid hex. + input: "(1684538768.521889) can0 200#8D64354J", + }, + { + name: "bad time", + // we destroy the time structure. + input: "(badtime.521889) can0 200#8D643546", + }, + { + name: "utf8 corruption", + // we attempt to mess up the data with broken utf8 + input: "(1684538768.521889) can0 200#8D6\xed\xa0\x8043546", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, ts, err := parseCanDumpLine(tt.input) + + if err == nil { + t.Fatalf("parseCanDumpLine() expected error but instead got f = %v, ts = %v", f, ts) + } + }) + } +}