// Copyright 2023 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 loadpe

import (
	"cmd/internal/objabi"
	"cmd/internal/sys"
	"cmd/link/internal/loader"
	"cmd/link/internal/sym"
	"fmt"
	"sort"
)

const (
	UNW_FLAG_EHANDLER  = 1 << 3
	UNW_FLAG_UHANDLER  = 2 << 3
	UNW_FLAG_CHAININFO = 4 << 3
	unwStaticDataSize  = 4 // Bytes of unwind data before the variable length part.
	unwCodeSize        = 2 // Bytes per unwind code.
)

// processSEH walks host-object pdata relocations and returns the set of
// per-entry pdata symbols created from the input section.
func processSEH(ldr *loader.Loader, arch *sys.Arch, pdata sym.LoaderSym) ([]loader.Sym, error) {
	switch arch.Family {
	case sys.AMD64:
		return processSEHAMD64(ldr, pdata)
	default:
		// TODO: support SEH on other architectures.
		return nil, fmt.Errorf("unsupported architecture for SEH: %v", arch.Family)
	}
}

func processSEHAMD64(ldr *loader.Loader, pdata sym.LoaderSym) ([]loader.Sym, error) {
	// The following loop traverses a list of pdata entries,
	// each entry being 3 relocations long. The first relocation
	// is a pointer to the function symbol to which the pdata entry
	// corresponds. The third relocation is a pointer to the
	// corresponding .xdata entry.
	// Reference:
	// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64#struct-runtime_function
	rels := ldr.Relocs(pdata)
	if rels.Count()%3 != 0 {
		return nil, fmt.Errorf(".pdata symbol %q has invalid relocation count", ldr.SymName(pdata))
	}
	data := ldr.Data(pdata)
	entries := make([]loader.Sym, 0, rels.Count()/3)

	for i := 0; i < rels.Count(); i += 3 {
		// Create a new symbol for the pdata entry.
		entry := ldr.MakeSymbolBuilder("")
		entry.SetType(sym.SSEHSECT)
		entry.SetAlign(4)
		entry.SetSize(12)
		entryOff := int(rels.At(i).Off())
		entryEnd := entryOff + 4*3
		entry.SetData(data[entryOff:entryEnd:entryEnd])

		// Add a relocation from the target function to the pdata entry
		// and to the exception handler, if present, to ensure they are
		// retained by dead code elimination.
		if targetFunc := rels.At(i).Sym(); targetFunc != 0 {
			sb := ldr.MakeSymbolUpdater(targetFunc)
			r, _ := sb.AddRel(objabi.R_KEEP)
			r.SetSym(entry.Sym())
			xrel := rels.At(i + 2)
			if xrel.Sym() != 0 {
				r, _ = sb.AddRel(objabi.R_KEEP)
				r.SetSym(xrel.Sym())
			}
			handler := findHandlerInXDataAMD64(ldr, xrel.Sym(), xrel.Add())
			if handler != 0 {
				r, _ = sb.AddRel(objabi.R_KEEP)
				r.SetSym(handler)
			}
		}

		// Copy the relocations from the original .pdata entry to the new symbol,
		// adjusting the offsets.
		for j := range 3 {
			rOld := rels.At(i + j)
			typ := rOld.Type()
			if rOld.Weak() {
				typ |= objabi.R_WEAK
			}
			rel, _ := entry.AddRel(typ)
			rel.SetOff(int32(j * 4))
			rel.SetSiz(rOld.Siz())
			rel.SetSym(rOld.Sym())
			rel.SetAdd(rOld.Add())
		}

		entries = append(entries, entry.Sym())
	}
	return entries, nil
}

// findHandlerInXDataAMD64 finds the symbol in the .xdata section that
// corresponds to the exception handler.
// Reference:
// https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64#struct-unwind_info
func findHandlerInXDataAMD64(ldr *loader.Loader, xsym sym.LoaderSym, add int64) loader.Sym {
	data := ldr.Data(xsym)
	if add < 0 || add+unwStaticDataSize > int64(len(data)) {
		return 0
	}
	data = data[add:]
	var isChained bool
	switch flag := data[0]; {
	case flag&UNW_FLAG_EHANDLER != 0 || flag&UNW_FLAG_UHANDLER != 0:
		// Exception handler.
	case flag&UNW_FLAG_CHAININFO != 0:
		isChained = true
	default:
		// Nothing to do.
		return 0
	}
	codes := data[2]
	if codes%2 != 0 {
		// There are always an even number of unwind codes, even if the last one is unused.
		codes += 1
	}
	// The exception handler relocation is the first relocation after the unwind codes,
	// unless it is chained, but we will handle this case later.
	targetOff := add + unwStaticDataSize + unwCodeSize*int64(codes)
	xrels := ldr.Relocs(xsym)
	xrelsCount := xrels.Count()
	idx := sort.Search(xrelsCount, func(i int) bool {
		return int64(xrels.At(i).Off()) >= targetOff
	})
	if idx == xrelsCount {
		return 0
	}
	if isChained {
		// The third relocations references the next .xdata entry in the chain, recurse.
		idx += 2
		if idx >= xrelsCount {
			return 0
		}
		r := xrels.At(idx)
		return findHandlerInXDataAMD64(ldr, r.Sym(), r.Add())
	}
	return xrels.At(idx).Sym()
}
