// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// The goroutines package contains utilities for tracking and getting active goroutines.
package goroutines

import (
	"bytes"
	"fmt"
	"runtime"
	"strconv"
)

// ScrubbedGoroutineDump returns either the current goroutine's stack or all
// goroutines' stacks, but with the actual values of arguments scrubbed out,
// lest it contain some private key material.
func ScrubbedGoroutineDump(all bool) []byte {
	var buf []byte
	// Grab stacks multiple times into increasingly larger buffer sizes
	// to minimize the risk that we blow past our iOS memory limit.
	for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
		buf = make([]byte, size)
		buf = buf[:runtime.Stack(buf, all)]
		if len(buf) < size {
			// It fit.
			break
		}
	}
	return scrubHex(buf)
}

func scrubHex(buf []byte) []byte {
	saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)

	foreachHexAddress(buf, func(in []byte) {
		if string(in) == "0x0" {
			return
		}
		if v, ok := saw[string(in)]; ok {
			for i := range in {
				in[i] = '_'
			}
			copy(in, v)
			return
		}
		inStr := string(in)
		u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
		for i := range in {
			in[i] = '_'
		}
		if err != nil {
			in[0] = '?'
			return
		}
		v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
		saw[inStr] = v
		copy(in, v)
	})
	return buf
}

var ohx = []byte("0x")

// foreachHexAddress calls f with each subslice of b that matches
// regexp `0x[0-9a-f]*`.
func foreachHexAddress(b []byte, f func([]byte)) {
	for len(b) > 0 {
		i := bytes.Index(b, ohx)
		if i == -1 {
			return
		}
		b = b[i:]
		hx := hexPrefix(b)
		f(hx)
		b = b[len(hx):]
	}
}

func hexPrefix(b []byte) []byte {
	for i, c := range b {
		if i < 2 {
			continue
		}
		if !isHexByte(c) {
			return b[:i]
		}
	}
	return b
}

func isHexByte(b byte) bool {
	return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
}
