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

//go:build !ios

package version

import (
	"bytes"
	"encoding/hex"
	"errors"
	"io"
	"os"
	"path"
	"runtime"
	"strings"
)

// CmdName returns either the base name of the current binary
// using os.Executable. If os.Executable fails (it shouldn't), then
// "cmd" is returned.
func CmdName() string {
	e, err := os.Executable()
	if err != nil {
		return "cmd"
	}
	return cmdName(e)
}

func cmdName(exe string) string {
	// fallbackName, the lowercase basename of the executable, is what we return if
	// we can't find the Go module metadata embedded in the file.
	fallbackName := prepExeNameForCmp(exe, runtime.GOARCH)

	var ret string
	info, err := findModuleInfo(exe)
	if err != nil {
		return fallbackName
	}
	// v is like:
	// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
	for _, line := range strings.Split(info, "\n") {
		if goPkg, ok := strings.CutPrefix(line, "path\t"); ok { // like "tailscale.com/cmd/tailscale"
			ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
			break
		}
	}
	if runtime.GOOS == "windows" && strings.HasPrefix(ret, "gui") && checkPreppedExeNameForGUI(fallbackName) {
		// The GUI binary for internal build system packaging reasons
		// has a path of "tailscale.io/win/gui".
		// Ignore that name and use fallbackName instead.
		return fallbackName
	}
	if ret == "" {
		return fallbackName
	}
	return ret
}

// findModuleInfo returns the Go module info from the executable file.
func findModuleInfo(file string) (s string, err error) {
	f, err := os.Open(file)
	if err != nil {
		return "", err
	}
	defer f.Close()
	// Scan through f until we find infoStart.
	buf := make([]byte, 65536)
	start, err := findOffset(f, buf, infoStart)
	if err != nil {
		return "", err
	}
	start += int64(len(infoStart))
	// Seek to the end of infoStart and scan for infoEnd.
	_, err = f.Seek(start, io.SeekStart)
	if err != nil {
		return "", err
	}
	end, err := findOffset(f, buf, infoEnd)
	if err != nil {
		return "", err
	}
	length := end - start
	// As of Aug 2021, tailscaled's mod info was about 2k.
	if length > int64(len(buf)) {
		return "", errors.New("mod info too large")
	}
	// We have located modinfo. Read it into buf.
	buf = buf[:length]
	_, err = f.Seek(start, io.SeekStart)
	if err != nil {
		return "", err
	}
	_, err = io.ReadFull(f, buf)
	if err != nil {
		return "", err
	}
	return string(buf), nil
}

// findOffset finds the absolute offset of needle in f,
// starting at f's current read position,
// using temporary buffer buf.
func findOffset(f *os.File, buf, needle []byte) (int64, error) {
	for {
		// Fill buf and look within it.
		n, err := f.Read(buf)
		if err != nil {
			return -1, err
		}
		i := bytes.Index(buf[:n], needle)
		if i < 0 {
			// Not found. Rewind a little bit in case we happened to end halfway through needle.
			rewind, err := f.Seek(int64(-len(needle)), io.SeekCurrent)
			if err != nil {
				return -1, err
			}
			// If we're at EOF and rewound exactly len(needle) bytes, return io.EOF.
			_, err = f.ReadAt(buf[:1], rewind+int64(len(needle)))
			if err == io.EOF {
				return -1, err
			}
			continue
		}
		// Found! Figure out exactly where.
		cur, err := f.Seek(0, io.SeekCurrent)
		if err != nil {
			return -1, err
		}
		return cur - int64(n) + int64(i), nil
	}
}

// These constants are taken from rsc.io/goversion.

var (
	infoStart, _ = hex.DecodeString("3077af0c9274080241e1c107e6d618e6")
	infoEnd, _   = hex.DecodeString("f932433186182072008242104116d8f2")
)
