package locspec import ( "errors" "fmt" "go/constant" "path" "path/filepath" "reflect" "regexp" "runtime" "strconv" "strings" "github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/service/api" ) const maxFindLocationCandidates = 5 // LocationSpec is an interface that represents a parsed location spec string. type LocationSpec interface { // Find returns all locations that match the location spec. Find(t *proc.Target, processArgs []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool, substitutePathRules [][2]string) ([]api.Location, string, error) } // NormalLocationSpec represents a basic location spec. // This can be a file:line or func:line. type NormalLocationSpec struct { Base string FuncBase *FuncLocationSpec LineOffset int } // RegexLocationSpec represents a regular expression // location expression such as /^myfunc$/. type RegexLocationSpec struct { FuncRegex string } // AddrLocationSpec represents an address when used // as a location spec. type AddrLocationSpec struct { AddrExpr string } // OffsetLocationSpec represents a location spec that // is an offset of the current location (file:line). type OffsetLocationSpec struct { Offset int } // LineLocationSpec represents a line number in the current file. type LineLocationSpec struct { Line int } // FuncLocationSpec represents a function in the target program. type FuncLocationSpec struct { PackageName string AbsolutePackage bool ReceiverName string PackageOrReceiverName string BaseName string } // Parse will turn locStr into a parsed LocationSpec. func Parse(locStr string) (LocationSpec, error) { rest := locStr malformed := func(reason string) error { //lint:ignore ST1005 backwards compatibility return fmt.Errorf("Malformed breakpoint location %q at %d: %s", locStr, len(locStr)-len(rest), reason) } if len(rest) == 0 { return nil, malformed("empty string") } switch rest[0] { case '+', '-': offset, err := strconv.Atoi(rest) if err != nil { return nil, malformed(err.Error()) } return &OffsetLocationSpec{offset}, nil case '/': if rest[len(rest)-1] == '/' { rx, rest := readRegex(rest[1:]) if len(rest) == 0 { return nil, malformed("non-terminated regular expression") } if len(rest) > 1 { return nil, malformed("no line offset can be specified for regular expression locations") } return &RegexLocationSpec{rx}, nil } else { return parseLocationSpecDefault(locStr, rest) } case '*': return &AddrLocationSpec{AddrExpr: rest[1:]}, nil default: return parseLocationSpecDefault(locStr, rest) } } func parseLocationSpecDefault(locStr, rest string) (LocationSpec, error) { malformed := func(reason string) error { //lint:ignore ST1005 backwards compatibility return fmt.Errorf("Malformed breakpoint location %q at %d: %s", locStr, len(locStr)-len(rest), reason) } v := strings.Split(rest, ":") if len(v) > 2 { // On Windows, path may contain ":", so split only on last ":" v = []string{strings.Join(v[0:len(v)-1], ":"), v[len(v)-1]} } if len(v) == 1 { n, err := strconv.ParseInt(v[0], 0, 64) if err == nil { return &LineLocationSpec{int(n)}, nil } } spec := &NormalLocationSpec{} spec.Base = v[0] spec.FuncBase = parseFuncLocationSpec(spec.Base) if len(v) < 2 { spec.LineOffset = -1 return spec, nil } rest = v[1] var err error spec.LineOffset, err = strconv.Atoi(rest) if err != nil || spec.LineOffset < 0 { return nil, malformed("line offset negative or not a number") } return spec, nil } func readRegex(in string) (rx string, rest string) { out := make([]rune, 0, len(in)) escaped := false for i, ch := range in { if escaped { if ch == '/' { out = append(out, '/') } else { out = append(out, '\\', ch) } escaped = false } else { switch ch { case '\\': escaped = true case '/': return string(out), in[i:] default: out = append(out, ch) } } } return string(out), "" } func parseFuncLocationSpec(in string) *FuncLocationSpec { var v []string pathend := strings.LastIndex(in, "/") if pathend < 0 { v = strings.Split(in, ".") } else { v = strings.Split(in[pathend:], ".") v[0] = in[:pathend] + v[0] } var spec FuncLocationSpec switch len(v) { case 1: spec.BaseName = v[0] case 2: spec.BaseName = v[1] r := stripReceiverDecoration(v[0]) if r != v[0] { spec.ReceiverName = r } else if strings.Contains(r, "/") { spec.PackageName = r } else { spec.PackageOrReceiverName = r } case 3: spec.BaseName = v[2] spec.ReceiverName = stripReceiverDecoration(v[1]) spec.PackageName = v[0] default: return nil } if strings.HasPrefix(spec.PackageName, "/") { spec.PackageName = spec.PackageName[1:] spec.AbsolutePackage = true } if strings.Contains(spec.BaseName, "/") || strings.Contains(spec.ReceiverName, "/") { return nil } return &spec } func stripReceiverDecoration(in string) string { if len(in) < 3 { return in } if (in[0] != '(') || (in[1] != '*') || (in[len(in)-1] != ')') { return in } return in[2 : len(in)-1] } // Match will return whether the provided function matches the location spec. func (spec *FuncLocationSpec) Match(sym *proc.Function, packageMap map[string][]string) bool { if spec.BaseName != sym.BaseName() { return false } recv := stripReceiverDecoration(sym.ReceiverName()) if spec.ReceiverName != "" && spec.ReceiverName != recv { return false } if spec.PackageName != "" { if spec.AbsolutePackage { if spec.PackageName != sym.PackageName() { return false } } else { if !packageMatch(spec.PackageName, sym.PackageName(), packageMap) { return false } } } if spec.PackageOrReceiverName != "" && !packageMatch(spec.PackageOrReceiverName, sym.PackageName(), packageMap) && spec.PackageOrReceiverName != recv { return false } return true } func packageMatch(specPkg, symPkg string, packageMap map[string][]string) bool { for _, pkg := range packageMap[specPkg] { if partialPackageMatch(pkg, symPkg) { return true } } return partialPackageMatch(specPkg, symPkg) } // Find will search all functions in the target program and filter them via the // regex location spec. Only functions matching the regex will be returned. func (loc *RegexLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool, _ [][2]string) ([]api.Location, string, error) { if scope == nil { //TODO(aarzilli): this needs only the list of function we should make it work return nil, "", errors.New("could not determine location (scope is nil)") } funcs := scope.BinInfo.Functions matches, err := regexFilterFuncs(loc.FuncRegex, funcs) if err != nil { return nil, "", err } r := make([]api.Location, 0, len(matches)) for i := range matches { addrs, _ := proc.FindFunctionLocation(t, matches[i], 0) if len(addrs) > 0 { r = append(r, addressesToLocation(addrs)) } } return r, "", nil } // Find returns the locations specified via the address location spec. func (loc *AddrLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool, _ [][2]string) ([]api.Location, string, error) { if scope == nil { addr, err := strconv.ParseInt(loc.AddrExpr, 0, 64) if err != nil { return nil, "", errors.New("could not determine current location (scope is nil)") } return []api.Location{{PC: uint64(addr)}}, "", nil } v, err := scope.EvalExpression(loc.AddrExpr, proc.LoadConfig{FollowPointers: true}) if err != nil { return nil, "", err } if v.Unreadable != nil { return nil, "", v.Unreadable } switch v.Kind { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: addr, _ := constant.Uint64Val(v.Value) return []api.Location{{PC: addr}}, "", nil case reflect.Func: fn := scope.BinInfo.PCToFunc(v.Base) pc, err := proc.FirstPCAfterPrologue(t, fn, false) if err != nil { return nil, "", err } return []api.Location{{PC: pc}}, v.Name, nil default: return nil, "", fmt.Errorf("wrong expression kind: %v", v.Kind) } } // FileMatch is true if the path matches the location spec. func (loc *NormalLocationSpec) FileMatch(path string) bool { return partialPathMatch(loc.Base, path) } func tryMatchRelativePathByProc(expr, debugname, file string) bool { return len(expr) > 0 && expr[0] == '.' && file == path.Join(path.Dir(debugname), expr) } func partialPathMatch(expr, path string) bool { if runtime.GOOS == "windows" { // Accept `expr` which is case-insensitive and slash-insensitive match to `path` expr = strings.ToLower(filepath.ToSlash(expr)) path = strings.ToLower(filepath.ToSlash(path)) } return partialPackageMatch(expr, path) } func partialPackageMatch(expr, path string) bool { if len(expr) < len(path)-1 { return strings.HasSuffix(path, expr) && (path[len(path)-len(expr)-1] == '/') } return expr == path } // AmbiguousLocationError is returned when the location spec // should only return one location but returns multiple instead. type AmbiguousLocationError struct { Location string CandidatesString []string CandidatesLocation []api.Location } func (ale AmbiguousLocationError) Error() string { var candidates []string if ale.CandidatesLocation != nil { for i := range ale.CandidatesLocation { candidates = append(candidates, ale.CandidatesLocation[i].Function.Name()) } } else { candidates = ale.CandidatesString } return fmt.Sprintf("Location %q ambiguous: %s…", ale.Location, strings.Join(candidates, ", ")) } // Find will return a list of locations that match the given location spec. // This matches each other location spec that does not already have its own spec // implemented (such as regex, or addr). func (loc *NormalLocationSpec) Find(t *proc.Target, processArgs []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool, substitutePathRules [][2]string) ([]api.Location, string, error) { limit := maxFindLocationCandidates var candidateFiles []string for _, sourceFile := range t.BinInfo().Sources { substFile := sourceFile if len(substitutePathRules) > 0 { substFile = SubstitutePath(sourceFile, substitutePathRules) } if loc.FileMatch(substFile) || (len(processArgs) >= 1 && tryMatchRelativePathByProc(loc.Base, processArgs[0], substFile)) { candidateFiles = append(candidateFiles, sourceFile) if len(candidateFiles) >= limit { break } } } limit -= len(candidateFiles) var candidateFuncs []string if loc.FuncBase != nil && limit > 0 { candidateFuncs = loc.findFuncCandidates(t.BinInfo(), limit) } if matching := len(candidateFiles) + len(candidateFuncs); matching == 0 { if scope == nil { return nil, "", fmt.Errorf("location %q not found", locStr) } // if no result was found this locations string could be an // expression that the user forgot to prefix with '*', try treating it as // such. addrSpec := &AddrLocationSpec{AddrExpr: locStr} locs, subst, err := addrSpec.Find(t, processArgs, scope, locStr, includeNonExecutableLines, nil) if err != nil { return nil, "", fmt.Errorf("location %q not found", locStr) } return locs, subst, nil } else if matching > 1 { return nil, "", AmbiguousLocationError{Location: locStr, CandidatesString: append(candidateFiles, candidateFuncs...)} } // len(candidateFiles) + len(candidateFuncs) == 1 var addrs []uint64 var err error if len(candidateFiles) == 1 { if loc.LineOffset < 0 { //lint:ignore ST1005 backwards compatibility return nil, "", errors.New("Malformed breakpoint location, no line offset specified") } addrs, err = proc.FindFileLocation(t, candidateFiles[0], loc.LineOffset) if includeNonExecutableLines { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { return []api.Location{{File: candidateFiles[0], Line: loc.LineOffset}}, "", nil } } } else { // len(candidateFuncs) == 1 addrs, err = proc.FindFunctionLocation(t, candidateFuncs[0], loc.LineOffset) } if err != nil { return nil, "", err } return []api.Location{addressesToLocation(addrs)}, "", nil } func (loc *NormalLocationSpec) findFuncCandidates(bi *proc.BinaryInfo, limit int) []string { candidateFuncs := map[string]struct{}{} // See if it matches generic functions first for fname := range bi.LookupGenericFunc() { if len(candidateFuncs) >= limit { break } if !loc.FuncBase.Match(&proc.Function{Name: fname}, bi.PackageMap) { continue } if loc.Base == fname { return []string{fname} } candidateFuncs[fname] = struct{}{} } for _, fns := range bi.LookupFunc() { f := fns[0] if len(candidateFuncs) >= limit { break } if !loc.FuncBase.Match(f, bi.PackageMap) { continue } if loc.Base == f.Name { // if an exact match for the function name is found use it return []string{f.Name} } // If f is an instantiation of a generic function see if we should add its generic version instead. if gn := f.NameWithoutTypeParams(); gn != "" { if _, alreadyAdded := candidateFuncs[gn]; !alreadyAdded { candidateFuncs[f.Name] = struct{}{} } } else { candidateFuncs[f.Name] = struct{}{} } } // convert candidateFuncs map into an array of its keys r := make([]string, 0, len(candidateFuncs)) for s := range candidateFuncs { r = append(r, s) } return r } // isAbs returns true if path looks like an absolute path. func isAbs(path string) bool { // Unix-like absolute path if strings.HasPrefix(path, "/") { return true } return windowsAbsPath(path) } func windowsAbsPath(path string) bool { // Windows UNC absolute path if strings.HasPrefix(path, `\\`) { return true } // DOS absolute paths if len(path) < 3 || path[1] != ':' { return false } return path[2] == '/' || path[2] == '\\' } func hasPathSeparatorSuffix(path string) bool { return strings.HasSuffix(path, "/") || strings.HasSuffix(path, "\\") } func hasPathSeparatorPrefix(path string) bool { return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "\\") } func pickSeparator(to string) string { var sep byte for i := range to { if to[i] == '/' || to[i] == '\\' { if sep == 0 { sep = to[i] } else if sep != to[i] { return "" } } } return string(sep) } func joinPath(to, rest string) string { sep := pickSeparator(to) switch sep { case "/": rest = strings.ReplaceAll(rest, "\\", sep) case "\\": rest = strings.ReplaceAll(rest, "/", sep) default: sep = "/" } toEndsWithSlash := hasPathSeparatorSuffix(to) restStartsWithSlash := hasPathSeparatorPrefix(rest) switch { case toEndsWithSlash && restStartsWithSlash: return to[:len(to)-1] + rest case toEndsWithSlash && !restStartsWithSlash: return to + rest case !toEndsWithSlash && restStartsWithSlash: return to + rest case !toEndsWithSlash && !restStartsWithSlash: fallthrough default: return to + sep + rest } } // SubstitutePath applies the specified path substitution rules to path. func SubstitutePath(path string, rules [][2]string) string { // Look for evidence that we are dealing with windows somewhere, if we are use case-insensitive matching caseInsensitive := windowsAbsPath(path) if !caseInsensitive { for i := range rules { if windowsAbsPath(rules[i][0]) || windowsAbsPath(rules[i][1]) { caseInsensitive = true break } } } for _, r := range rules { from, to := r[0], r[1] // if we have an exact match, use it directly. if path == from { return to } match := false var rest string if from == "" { match = !isAbs(path) rest = path } else { if caseInsensitive { match = strings.HasPrefix(strings.ToLower(path), strings.ToLower(from)) if match { path = strings.ToLower(path) from = strings.ToLower(from) } } else { match = strings.HasPrefix(path, from) } if match { // make sure the match ends on something that looks like a path separator boundary rest = path[len(from):] match = hasPathSeparatorSuffix(from) || hasPathSeparatorPrefix(rest) } } if match { if to == "" { // make sure we return a relative path, regardless of whether 'from' consumed a final / or not if hasPathSeparatorPrefix(rest) { return rest[1:] } return rest } return joinPath(to, rest) } } return path } func addressesToLocation(addrs []uint64) api.Location { if len(addrs) == 0 { return api.Location{} } return api.Location{PC: addrs[0], PCs: addrs} } // Find returns the location after adding the offset amount to the current line number. func (loc *OffsetLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, _ string, includeNonExecutableLines bool, _ [][2]string) ([]api.Location, string, error) { if scope == nil { return nil, "", errors.New("could not determine current location (scope is nil)") } file, line, fn := scope.BinInfo.PCToLine(scope.PC) if loc.Offset == 0 { subst := "" if fn != nil { subst = fmt.Sprintf("%s:%d", file, line) } return []api.Location{{PC: scope.PC}}, subst, nil } if fn == nil { return nil, "", errors.New("could not determine current location") } subst := fmt.Sprintf("%s:%d", file, line+loc.Offset) addrs, err := proc.FindFileLocation(t, file, line+loc.Offset) if includeNonExecutableLines { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { return []api.Location{{File: file, Line: line + loc.Offset}}, subst, nil } } return []api.Location{addressesToLocation(addrs)}, subst, err } // Find will return the location at the given line in the current file. func (loc *LineLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, _ string, includeNonExecutableLines bool, _ [][2]string) ([]api.Location, string, error) { if scope == nil { return nil, "", errors.New("could not determine current location (scope is nil)") } file, _, fn := scope.BinInfo.PCToLine(scope.PC) if fn == nil { return nil, "", errors.New("could not determine current location") } subst := fmt.Sprintf("%s:%d", file, loc.Line) addrs, err := proc.FindFileLocation(t, file, loc.Line) if includeNonExecutableLines { if _, isCouldNotFindLine := err.(*proc.ErrCouldNotFindLine); isCouldNotFindLine { return []api.Location{{File: file, Line: loc.Line}}, subst, nil } } return []api.Location{addressesToLocation(addrs)}, subst, err } func regexFilterFuncs(filter string, allFuncs []proc.Function) ([]string, error) { regex, err := regexp.Compile(filter) if err != nil { return nil, fmt.Errorf("invalid filter argument: %s", err.Error()) } funcs := []string{} for _, f := range allFuncs { if regex.MatchString(f.Name) { funcs = append(funcs, f.Name) } } return funcs, nil }