// Copyright 2017 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 version reports the Go version used to build program executables.
package version

import (
	"bytes"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"strings"
)

// Version is the information reported by ReadExe.
type Version struct {
	Release        string // Go version (runtime.Version in the program)
	ModuleInfo     string // program's module information
	BoringCrypto   bool   // program uses BoringCrypto
	StandardCrypto bool   // program uses standard crypto (replaced by BoringCrypto)
	FIPSOnly       bool   // program imports "crypto/tls/fipsonly"
}

// ReadExe reports information about the Go version used to build
// the program executable named by file.
func ReadExe(file string) (Version, error) {
	f, err := os.Open(file)
	if err != nil {
		return Version{}, err
	}
	defer f.Close()

	return ReadExeFromReader(f)
}

func ReadExeFromReader(reader io.ReaderAt) (Version, error) {
	var v Version
	f, err := parseExe(reader)
	if err != nil {
		return v, err
	}
	isGo := false
	for _, name := range f.SectionNames() {
		if name == ".note.go.buildid" {
			isGo = true
		}
	}
	syms, symsErr := f.Symbols()
	isGccgo := false
	for _, sym := range syms {
		name := sym.Name
		if name == "runtime.main" || name == "main.main" {
			isGo = true
		}
		if strings.HasPrefix(name, "runtime.") && strings.HasSuffix(name, "$descriptor") {
			isGccgo = true
		}
		if name == "runtime.buildVersion" {
			isGo = true
			release, err := readBuildVersion(f, sym.Addr, sym.Size)
			if err != nil {
				return v, err
			}
			v.Release = release
		}
		// Note: Using strings.HasPrefix because Go 1.17+ adds ".abi0" to many of these symbols.
		if strings.Contains(name, "_Cfunc__goboringcrypto_") || strings.HasPrefix(name, "crypto/internal/boring/sig.BoringCrypto") {
			v.BoringCrypto = true
		}
		if strings.HasPrefix(name, "crypto/internal/boring/sig.FIPSOnly") {
			v.FIPSOnly = true
		}
		for _, re := range standardCryptoNames {
			if re.MatchString(name) {
				v.StandardCrypto = true
			}
		}
		if strings.HasPrefix(name, "crypto/internal/boring/sig.StandardCrypto") {
			v.StandardCrypto = true
		}
	}

	if DebugMatch {
		v.Release = ""
	}
	if err := findModuleInfo(&v, f); err != nil {
		return v, err
	}
	if v.Release == "" {
		g, release := readBuildVersionX86Asm(f)
		if g {
			isGo = true
			v.Release = release
			if err := findCryptoSigs(&v, f); err != nil {
				return v, err
			}
		}
	}
	if isGccgo && v.Release == "" {
		isGo = true
		v.Release = "gccgo (version unknown)"
	}
	if !isGo && symsErr != nil {
		return v, symsErr
	}

	if !isGo {
		return v, errors.New("not a Go executable")
	}
	if v.Release == "" {
		v.Release = "unknown Go version"
	}
	return v, nil
}

var re = regexp.MustCompile

var standardCryptoNames = []*regexp.Regexp{
	re(`^crypto/sha1\.\(\*digest\)`),
	re(`^crypto/sha256\.\(\*digest\)`),
	re(`^crypto/rand\.\(\*devReader\)`),
	re(`^crypto/rsa\.encrypt(\.abi.)?$`),
	re(`^crypto/rsa\.decrypt(\.abi.)?$`),
}

func readBuildVersion(f exe, addr, size uint64) (string, error) {
	if size == 0 {
		size = uint64(f.AddrSize() * 2)
	}
	if size != 8 && size != 16 {
		return "", fmt.Errorf("invalid size for runtime.buildVersion")
	}
	data, err := f.ReadData(addr, size)
	if err != nil {
		return "", fmt.Errorf("reading runtime.buildVersion: %v", err)
	}

	if size == 8 {
		addr = uint64(f.ByteOrder().Uint32(data))
		size = uint64(f.ByteOrder().Uint32(data[4:]))
	} else {
		addr = f.ByteOrder().Uint64(data)
		size = f.ByteOrder().Uint64(data[8:])
	}
	if size > 1000 {
		return "", fmt.Errorf("implausible string size %d for runtime.buildVersion", size)
	}

	data, err = f.ReadData(addr, size)
	if err != nil {
		return "", fmt.Errorf("reading runtime.buildVersion string data: %v", err)
	}
	return string(data), nil
}

// Code signatures that indicate BoringCrypto or crypto/internal/fipsonly.
// These are not byte literals in order to avoid the actual
// byte signatures appearing in the goversion binary,
// because on some systems you can't tell rodata from text.
var (
	sigBoringCrypto, _   = hex.DecodeString("EB1DF448F44BF4B332F52813A3B450D441CC2485F001454E92101B1D2F1950C3")
	sigStandardCrypto, _ = hex.DecodeString("EB1DF448F44BF4BAEE4DFA9851CA56A91145E83E99C59CF911CB8E80DAF12FC3")
	sigFIPSOnly, _       = hex.DecodeString("EB1DF448F44BF4363CB9CE9D68047D31F28D325D5CA5873F5D80CAF6D6151BC3")
)

func findCryptoSigs(v *Version, f exe) error {
	const maxSigLen = 1 << 10
	start, end := f.TextRange()
	for addr := start; addr < end; {
		size := uint64(1 << 20)
		if end-addr < size {
			size = end - addr
		}
		data, err := f.ReadData(addr, size)
		if err != nil {
			return fmt.Errorf("reading text: %v", err)
		}
		if haveSig(data, sigBoringCrypto) {
			v.BoringCrypto = true
		}
		if haveSig(data, sigFIPSOnly) {
			v.FIPSOnly = true
		}
		if haveSig(data, sigStandardCrypto) {
			v.StandardCrypto = true
		}
		if addr+size < end {
			size -= maxSigLen
		}
		addr += size
	}
	return nil
}

func haveSig(data, sig []byte) bool {
	const align = 16
	for {
		i := bytes.Index(data, sig)
		if i < 0 {
			return false
		}
		if i&(align-1) == 0 {
			return true
		}
		// Found unaligned match; unexpected but
		// skip to next aligned boundary and keep searching.
		data = data[(i+align-1)&^(align-1):]
	}
}

func findModuleInfo(v *Version, f exe) error {
	const maxModInfo = 128 << 10
	start, end := f.RODataRange()
	for addr := start; addr < end; {
		size := uint64(4 << 20)
		if end-addr < size {
			size = end - addr
		}
		data, err := f.ReadData(addr, size)
		if err != nil {
			return fmt.Errorf("reading text: %v", err)
		}
		if haveModuleInfo(data, v) {
			return nil
		}
		if addr+size < end {
			size -= maxModInfo
		}
		addr += size
	}
	return nil
}

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

func haveModuleInfo(data []byte, v *Version) bool {
	i := bytes.Index(data, infoStart)
	if i < 0 {
		return false
	}
	j := bytes.Index(data[i:], infoEnd)
	if j < 0 {
		return false
	}
	v.ModuleInfo = string(data[i+len(infoStart) : i+j])
	return true
}
