// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package raw import ( "bufio" "fmt" "io" "strconv" "strings" "unicode" "internal/trace/v2/event" "internal/trace/v2/version" ) // TextReader parses a text format trace with only very basic validation // into an event stream. type TextReader struct { v version.Version specs []event.Spec names map[string]event.Type s *bufio.Scanner } // NewTextReader creates a new reader for the trace text format. func NewTextReader(r io.Reader) (*TextReader, error) { tr := &TextReader{s: bufio.NewScanner(r)} line, err := tr.nextLine() if err != nil { return nil, err } trace, line := readToken(line) if trace != "Trace" { return nil, fmt.Errorf("failed to parse header") } gover, line := readToken(line) if !strings.HasPrefix(gover, "Go1.") { return nil, fmt.Errorf("failed to parse header Go version") } rawv, err := strconv.ParseUint(gover[len("Go1."):], 10, 64) if err != nil { return nil, fmt.Errorf("failed to parse header Go version: %v", err) } v := version.Version(rawv) if !v.Valid() { return nil, fmt.Errorf("unknown or unsupported Go version 1.%d", v) } tr.v = v tr.specs = v.Specs() tr.names = event.Names(tr.specs) for _, r := range line { if !unicode.IsSpace(r) { return nil, fmt.Errorf("encountered unexpected non-space at the end of the header: %q", line) } } return tr, nil } // Version returns the version of the trace that we're reading. func (r *TextReader) Version() version.Version { return r.v } // ReadEvent reads and returns the next trace event in the text stream. func (r *TextReader) ReadEvent() (Event, error) { line, err := r.nextLine() if err != nil { return Event{}, err } evStr, line := readToken(line) ev, ok := r.names[evStr] if !ok { return Event{}, fmt.Errorf("unidentified event: %s", evStr) } spec := r.specs[ev] args, err := readArgs(line, spec.Args) if err != nil { return Event{}, fmt.Errorf("reading args for %s: %v", evStr, err) } if spec.IsStack { len := int(args[1]) for i := 0; i < len; i++ { line, err := r.nextLine() if err == io.EOF { return Event{}, fmt.Errorf("unexpected EOF while reading stack: args=%v", args) } if err != nil { return Event{}, err } frame, err := readArgs(line, frameFields) if err != nil { return Event{}, err } args = append(args, frame...) } } var data []byte if spec.HasData { line, err := r.nextLine() if err == io.EOF { return Event{}, fmt.Errorf("unexpected EOF while reading data for %s: args=%v", evStr, args) } if err != nil { return Event{}, err } data, err = readData(line) if err != nil { return Event{}, err } } return Event{ Version: r.v, Ev: ev, Args: args, Data: data, }, nil } func (r *TextReader) nextLine() (string, error) { for { if !r.s.Scan() { if err := r.s.Err(); err != nil { return "", err } return "", io.EOF } txt := r.s.Text() tok, _ := readToken(txt) if tok == "" { continue // Empty line or comment. } return txt, nil } } var frameFields = []string{"pc", "func", "file", "line"} func readArgs(s string, names []string) ([]uint64, error) { var args []uint64 for _, name := range names { arg, value, rest, err := readArg(s) if err != nil { return nil, err } if arg != name { return nil, fmt.Errorf("expected argument %q, but got %q", name, arg) } args = append(args, value) s = rest } for _, r := range s { if !unicode.IsSpace(r) { return nil, fmt.Errorf("encountered unexpected non-space at the end of an event: %q", s) } } return args, nil } func readArg(s string) (arg string, value uint64, rest string, err error) { var tok string tok, rest = readToken(s) if len(tok) == 0 { return "", 0, s, fmt.Errorf("no argument") } parts := strings.SplitN(tok, "=", 2) if len(parts) < 2 { return "", 0, s, fmt.Errorf("malformed argument: %q", tok) } arg = parts[0] value, err = strconv.ParseUint(parts[1], 10, 64) if err != nil { return arg, value, s, fmt.Errorf("failed to parse argument value %q for arg %q", parts[1], parts[0]) } return } func readToken(s string) (token, rest string) { tkStart := -1 for i, r := range s { if r == '#' { return "", "" } if !unicode.IsSpace(r) { tkStart = i break } } if tkStart < 0 { return "", "" } tkEnd := -1 for i, r := range s[tkStart:] { if unicode.IsSpace(r) || r == '#' { tkEnd = i + tkStart break } } if tkEnd < 0 { return s[tkStart:], "" } return s[tkStart:tkEnd], s[tkEnd:] } func readData(line string) ([]byte, error) { parts := strings.SplitN(line, "=", 2) if len(parts) < 2 || strings.TrimSpace(parts[0]) != "data" { return nil, fmt.Errorf("malformed data: %q", line) } data, err := strconv.Unquote(strings.TrimSpace(parts[1])) if err != nil { return nil, fmt.Errorf("failed to parse data: %q: %v", line, err) } return []byte(data), nil }