--- /dev/null
+// Copyright 2020 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 stack
+
+import (
+ "bufio"
+ "errors"
+ "io"
+ "regexp"
+ "strconv"
+)
+
+var (
+ reBlank = regexp.MustCompile(`^\s*$`)
+ reGoroutine = regexp.MustCompile(`^\s*goroutine (\d+) \[([^\]]*)\]:\s*$`)
+ reCall = regexp.MustCompile(`^\s*` +
+ `(created by )?` + //marker
+ `(([\w/.]+/)?[\w]+)\.` + //package
+ `(\(([^:.)]*)\)\.)?` + //optional type
+ `([\w\.]+)` + //function
+ `(\(.*\))?` + // args
+ `\s*$`)
+ rePos = regexp.MustCompile(`^\s*(.*):(\d+)( .*)?$`)
+
+ errBreakParse = errors.New("break parse")
+)
+
+// Scanner splits an input stream into lines in a way that is consumable by
+// the parser.
+type Scanner struct {
+ lines *bufio.Scanner
+ done bool
+}
+
+// NewScanner creates a scanner on top of a reader.
+func NewScanner(r io.Reader) *Scanner {
+ s := &Scanner{
+ lines: bufio.NewScanner(r),
+ }
+ s.Skip() // prefill
+ return s
+}
+
+// Peek returns the next line without consuming it.
+func (s *Scanner) Peek() string {
+ if s.done {
+ return ""
+ }
+ return s.lines.Text()
+}
+
+// Skip consumes the next line without looking at it.
+// Normally used after it has already been looked at using Peek.
+func (s *Scanner) Skip() {
+ if !s.lines.Scan() {
+ s.done = true
+ }
+}
+
+// Next consumes and returns the next line.
+func (s *Scanner) Next() string {
+ line := s.Peek()
+ s.Skip()
+ return line
+}
+
+// Done returns true if the scanner has reached the end of the underlying
+// stream.
+func (s *Scanner) Done() bool {
+ return s.done
+}
+
+// Err returns true if the scanner has reached the end of the underlying
+// stream.
+func (s *Scanner) Err() error {
+ return s.lines.Err()
+}
+
+// Match returns the submatchs of the regular expression against the next line.
+// If it matched the line is also consumed.
+func (s *Scanner) Match(re *regexp.Regexp) []string {
+ if s.done {
+ return nil
+ }
+ match := re.FindStringSubmatch(s.Peek())
+ if match != nil {
+ s.Skip()
+ }
+ return match
+}
+
+// SkipBlank skips any number of pure whitespace lines.
+func (s *Scanner) SkipBlank() {
+ for !s.done {
+ line := s.Peek()
+ if len(line) != 0 && !reBlank.MatchString(line) {
+ return
+ }
+ s.Skip()
+ }
+}
+
+// Parse the current contiguous block of goroutine stack traces until the
+// scanned content no longer matches.
+func Parse(scanner *Scanner) (Dump, error) {
+ dump := Dump{}
+ for {
+ gr, ok := parseGoroutine(scanner)
+ if !ok {
+ return dump, nil
+ }
+ dump = append(dump, gr)
+ }
+}
+
+func parseGoroutine(scanner *Scanner) (Goroutine, bool) {
+ match := scanner.Match(reGoroutine)
+ if match == nil {
+ return Goroutine{}, false
+ }
+ id, _ := strconv.ParseInt(match[1], 0, 32)
+ gr := Goroutine{
+ ID: int(id),
+ State: match[2],
+ }
+ for {
+ frame, ok := parseFrame(scanner)
+ if !ok {
+ scanner.SkipBlank()
+ return gr, true
+ }
+ if frame.Position.Filename != "" {
+ gr.Stack = append(gr.Stack, frame)
+ }
+ }
+}
+
+func parseFrame(scanner *Scanner) (Frame, bool) {
+ fun, ok := parseFunction(scanner)
+ if !ok {
+ return Frame{}, false
+ }
+ frame := Frame{
+ Function: fun,
+ }
+ frame.Position, ok = parsePosition(scanner)
+ // if ok is false, then this is a broken state.
+ // we got the func but not the file that must follow
+ // the consumed line can be recovered from the frame
+ //TODO: push back the fun raw
+ return frame, ok
+}
+
+func parseFunction(scanner *Scanner) (Function, bool) {
+ match := scanner.Match(reCall)
+ if match == nil {
+ return Function{}, false
+ }
+ return Function{
+ Package: match[2],
+ Type: match[5],
+ Name: match[6],
+ }, true
+}
+
+func parsePosition(scanner *Scanner) (Position, bool) {
+ match := scanner.Match(rePos)
+ if match == nil {
+ return Position{}, false
+ }
+ line, _ := strconv.ParseInt(match[2], 0, 32)
+ return Position{Filename: match[1], Line: int(line)}, true
+}