| // 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/tracev2" | |
| "internal/trace/version" | |
| ) | |
| // TextReader parses a text format trace with only very basic validation | |
| // into an event stream. | |
| type TextReader struct { | |
| v version.Version | |
| specs []tracev2.EventSpec | |
| names map[string]tracev2.EventType | |
| 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 = tracev2.EventNames(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") | |
| } | |
| arg, val, found := strings.Cut(tok, "=") | |
| if !found { | |
| return "", 0, s, fmt.Errorf("malformed argument: %q", tok) | |
| } | |
| value, err = strconv.ParseUint(val, 10, 64) | |
| if err != nil { | |
| return arg, value, s, fmt.Errorf("failed to parse argument value %q for arg %q", val, arg) | |
| } | |
| 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) { | |
| dk, dv, found := strings.Cut(line, "=") | |
| if !found || strings.TrimSpace(dk) != "data" { | |
| return nil, fmt.Errorf("malformed data: %q", line) | |
| } | |
| data, err := strconv.Unquote(strings.TrimSpace(dv)) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to parse data: %q: %v", line, err) | |
| } | |
| return []byte(data), nil | |
| } |