package winpwn

import (
	"debug/pe"
	"fmt"
	"os"
	"regexp"
	"sort"
	"strings"

	"golang.org/x/arch/x86/x86asm"
)

// Gadget describes a found ROP gadget.
type Gadget struct {
	Address      uint64
	Instructions string
}

// ROP finds ROP gadgets in a binary, the winpwn analogue of pwntools' ROP.
// NewROP shells out to rp-win.exe (a Windows build of rp++) — a real,
// battle-tested gadget finder instead of a hand-rolled scanner. Disassemble
// still works natively (via golang.org/x/arch/x86/x86asm) for verifying a
// chain in-script.
type ROP struct {
	binaryPath string
	toolPath   string
	gadgets    []Gadget
	pe         *PEFile // held open for Disassemble() and arch detection
	mode       int     // x86asm.Mode equivalent (32/64)
}

// defaultRPWinTool is where rp-win.exe lives on this machine. Override with
// the RP_WIN_EXE environment variable if it's installed somewhere else.
const defaultRPWinTool = `C:\tools\rp-win\rp-win.exe`

// NewROP scans binaryPath for ROP/JOP gadgets using rp-win.exe, resolved
// from the RP_WIN_EXE environment variable or defaultRPWinTool. Use
// NewROPExternal to point at a specific tool binary instead (a different
// rp++ build, or a copy kept somewhere else).
func NewROP(binaryPath string) (*ROP, error) {
	toolPath := os.Getenv("RP_WIN_EXE")
	if toolPath == "" {
		toolPath = defaultRPWinTool
	}
	if _, err := os.Stat(toolPath); err != nil {
		return nil, fmt.Errorf("can't find rp-win.exe at %q (%w) -- set RP_WIN_EXE to override, or use NewROPExternal(path, toolPath)", toolPath, err)
	}
	return newROP(binaryPath, toolPath)
}

func newROP(binaryPath, toolPath string) (*ROP, error) {
	peFile, err := OpenPE(binaryPath)
	if err != nil {
		return nil, err
	}

	mode, err := archMode(peFile)
	if err != nil {
		peFile.Close()
		return nil, err
	}

	r := &ROP{binaryPath: binaryPath, toolPath: toolPath, pe: peFile, mode: mode}
	if err := r.findGadgetsExternal(); err != nil {
		peFile.Close()
		return nil, err
	}
	return r, nil
}

// Close releases the PE handle held for Disassemble/arch detection.
func (r *ROP) Close() {
	if r.pe != nil {
		r.pe.Close()
	}
}

func archMode(p *PEFile) (int, error) {
	switch p.File.Machine {
	case pe.IMAGE_FILE_MACHINE_AMD64:
		return 64, nil
	case pe.IMAGE_FILE_MACHINE_I386:
		return 32, nil
	default:
		return 0, fmt.Errorf("unsupported machine type for gadget scanning: 0x%X", p.File.Machine)
	}
}

// Disassemble decodes up to count instructions starting at the absolute
// address addr, the verification step pwntools leaves to objdump/Capstone:
// "did the gadget chain I built actually decode the way I think it did".
func (r *ROP) Disassemble(addr uint64, count int) ([]string, error) {
	imageBase, err := r.pe.ImageBase()
	if err != nil {
		return nil, err
	}
	if addr < imageBase {
		return nil, fmt.Errorf("address is below ImageBase")
	}
	rva := uint32(addr - imageBase)
	offset := r.pe.RVAToFileOffset(rva)
	if offset == 0 {
		return nil, fmt.Errorf("address does not map to any section")
	}

	buf := make([]byte, 16*count)
	n, _ := r.pe.r.ReadAt(buf, offset)
	buf = buf[:n]

	var lines []string
	pos := 0
	for i := 0; i < count && pos < len(buf); i++ {
		inst, err := x86asm.Decode(buf[pos:], r.mode)
		if err != nil {
			return lines, fmt.Errorf("decode failed at +%d: %w", pos, err)
		}
		lines = append(lines, strings.ToLower(x86asm.IntelSyntax(inst, addr+uint64(pos), nil)))
		pos += inst.Len
	}
	return lines, nil
}

// Find returns every gadget whose formatted instruction text contains
// pattern as a substring, ranked so index 0 is the best candidate to use --
// the one-expression version of Search for the common case:
//
//	rop.Find("pop rcx ; ret")[0].Address
//
// Ranking, in order: an exact match to pattern beats a mere substring match
// (a bare "pop rcx ; ret" outranks "ror byte [rax-0x1], 0x15 ; pop rcx ;
// ret" even though both contain the pattern and the latter may sit at a
// numerically lower address); shorter instruction text (fewer side-effect
// instructions riding along) beats longer; address ascending breaks
// remaining ties for determinism. Without this, sorting by raw address
// alone can hand back a "dirty" multi-instruction gadget at [0] purely
// because it happens to start a few bytes earlier in memory.
//
// Indexing an empty result panics -- deliberate for exploit scripts: fail
// loudly at the gadget lookup itself, not three chain-steps later against a
// garbage address.
func (r *ROP) Find(pattern string) []Gadget {
	needle := strings.ToLower(pattern)
	var out []Gadget
	for _, g := range r.gadgets {
		if strings.Contains(strings.ToLower(g.Instructions), needle) {
			out = append(out, g)
		}
	}
	sort.Slice(out, func(i, j int) bool {
		ei, ej := strings.EqualFold(out[i].Instructions, pattern), strings.EqualFold(out[j].Instructions, pattern)
		if ei != ej {
			return ei
		}
		if len(out[i].Instructions) != len(out[j].Instructions) {
			return len(out[i].Instructions) < len(out[j].Instructions)
		}
		return out[i].Address < out[j].Address
	})
	return out
}

// SearchRegex finds gadgets whose instruction text matches the given
// regular expression (e.g. `^pop r[a-z]+ ; ret$`), for when a plain
// substring (Search/Find) isn't precise enough.
func (r *ROP) SearchRegex(pattern string) ([]Gadget, error) {
	re, err := regexp.Compile(pattern)
	if err != nil {
		return nil, err
	}

	var results []Gadget
	for _, g := range r.gadgets {
		if re.MatchString(strings.ToLower(g.Instructions)) {
			results = append(results, g)
		}
	}
	if len(results) == 0 {
		return nil, fmt.Errorf("no gadget matched regex %q", pattern)
	}
	return results, nil
}
