package winpwn

import (
	"bytes"
	"debug/pe"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"os"
)

// PEFile is a read-only-by-default handle on a PE32/PE32+ image, the winpwn
// analogue of pwntools' ELF. Unlike ELF, a PE's bytes can come from two
// meaningfully different places: a file on disk (OpenPE) or a loaded
// module's live address space inside a running process (OpenPEFromProcess).
// Every accessor in this package (checksec, IAT/EAT, gadget scanning,
// patching) is written against the r/w fields below so both backings get
// every feature for free.
type PEFile struct {
	File *pe.File

	r      io.ReaderAt
	w      io.WriterAt // nil when opened read-only
	closer io.Closer   // nil if there is nothing to close
	size   int64       // total backing size, for whole-image operations (checksum recompute)

	// live is true when r/w address a process's memory (OpenPEFromProcess)
	// rather than a file's bytes (OpenPE/OpenPEForWrite). See
	// RVAToFileOffset for why this changes the RVA translation.
	live bool
}

func OpenPE(path string) (*PEFile, error) {
	fd, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	f, err := pe.NewFile(fd)
	if err != nil {
		fd.Close()
		return nil, err
	}
	info, err := fd.Stat()
	if err != nil {
		fd.Close()
		return nil, err
	}
	return &PEFile{File: f, r: fd, closer: fd, size: info.Size()}, nil
}

func (p *PEFile) Close() {
	if p.closer != nil {
		p.closer.Close()
	}
}

// IsLive reports whether this PEFile is backed by a running process's
// address space (OpenPEFromProcess) rather than a file on disk.
func (p *PEFile) IsLive() bool {
	return p.live
}

// peHeader normalizes the PE32 (32-bit) vs PE32+ (64-bit) optional header
// split into the fields callers actually need, so the rest of the package
// doesn't have to type-switch on pe.OptionalHeader32/64 everywhere.
type peHeader struct {
	is64                bool
	imageBase           uint64
	addressOfEntryPoint uint32
	sizeOfImage         uint32
	dllCharacteristics  uint16
	dataDirectory       [16]pe.DataDirectory
}

func (p *PEFile) header() (peHeader, error) {
	switch oh := p.File.OptionalHeader.(type) {
	case *pe.OptionalHeader64:
		return peHeader{
			is64:                true,
			imageBase:           oh.ImageBase,
			addressOfEntryPoint: oh.AddressOfEntryPoint,
			sizeOfImage:         oh.SizeOfImage,
			dllCharacteristics:  oh.DllCharacteristics,
			dataDirectory:       oh.DataDirectory,
		}, nil
	case *pe.OptionalHeader32:
		return peHeader{
			is64:                false,
			imageBase:           uint64(oh.ImageBase),
			addressOfEntryPoint: oh.AddressOfEntryPoint,
			sizeOfImage:         oh.SizeOfImage,
			dllCharacteristics:  oh.DllCharacteristics,
			dataDirectory:       oh.DataDirectory,
		}, nil
	default:
		return peHeader{}, errors.New("unrecognized PE optional header (not PE32 or PE32+)")
	}
}

// Is64Bit reports whether this is a PE32+ (x64) image.
func (p *PEFile) Is64Bit() (bool, error) {
	h, err := p.header()
	if err != nil {
		return false, err
	}
	return h.is64, nil
}

// ImageBase returns the preferred load address from the PE optional header
// (the Go analogue of pwntools' ELF.address when ASLR is disabled). For a
// live-process-backed PEFile this is still the header's *preferred* base,
// not necessarily where the module actually landed -- use the base passed
// to OpenPEFromProcess (e.g. from ResolveModuleBase) for the real address.
func (p *PEFile) ImageBase() (uint64, error) {
	h, err := p.header()
	if err != nil {
		return 0, err
	}
	return h.imageBase, nil
}

// EntryPoint returns the absolute address of the entry point
// (ImageBase + AddressOfEntryPoint).
func (p *PEFile) EntryPoint() (uint64, error) {
	h, err := p.header()
	if err != nil {
		return 0, err
	}
	return h.imageBase + uint64(h.addressOfEntryPoint), nil
}

// RVAToFileOffset converts a relative virtual address into the offset to
// pass to this PEFile's backing ReaderAt/WriterAt.
//
// For a disk-backed PEFile (OpenPE/OpenPEForWrite) this walks the section
// table to translate an RVA into a PointerToRawData-relative file offset --
// necessary because SectionAlignment and FileAlignment differ, so a
// section's position in the loaded image and its position on disk aren't
// the same number.
//
// For a live-process-backed PEFile (OpenPEFromProcess) the backing
// ReaderAt/WriterAt already treats offset 0 as the module's base address,
// so "offset" already *is* the RVA: that's the entire definition of a
// relative virtual address once the image is actually loaded. This is the
// identity function in that case.
func (p *PEFile) RVAToFileOffset(rva uint32) int64 {
	if p.live {
		return int64(rva)
	}
	for _, sec := range p.File.Sections {
		if rva >= sec.VirtualAddress && rva < sec.VirtualAddress+sec.VirtualSize {
			return int64(rva - sec.VirtualAddress + sec.Offset)
		}
	}
	return 0
}

// readCString reads a null-terminated ASCII string starting at the given
// read offset (file offset, or RVA for a live-backed PEFile -- see
// RVAToFileOffset).
func (p *PEFile) readCString(offset int64) (string, error) {
	var out []byte
	buf := make([]byte, 1)
	for {
		if _, err := p.r.ReadAt(buf, offset); err != nil {
			if err == io.EOF {
				break
			}
			return "", err
		}
		if buf[0] == 0 {
			break
		}
		out = append(out, buf[0])
		offset++
	}
	return string(out), nil
}

// readStructAt fills v (a pointer to a fixed-size struct of fixed-width
// fields) by reading binary.Size(v) bytes at offset through this PEFile's
// backing ReaderAt. The one read-offset helper every struct-shaped PE
// directory parse in this package goes through, so disk and live-process
// backings share the exact same parsing code.
func (p *PEFile) readStructAt(offset int64, v any) error {
	size := binary.Size(v)
	if size < 0 {
		return errors.New("readStructAt: unsupported type")
	}
	buf := make([]byte, size)
	if _, err := p.r.ReadAt(buf, offset); err != nil {
		return err
	}
	return binary.Read(bytes.NewReader(buf), binary.LittleEndian, v)
}

// writeAt writes data at offset through this PEFile's backing WriterAt,
// failing clearly if the PEFile was opened read-only.
func (p *PEFile) writeAt(offset int64, data []byte) error {
	if p.w == nil {
		return errors.New("PEFile is read-only; open with OpenPEForWrite or OpenPEFromProcess(write) for write access")
	}
	_, err := p.w.WriteAt(data, offset)
	return err
}

// GetProcAddress looks up a function's RVA by name, the bare-bones analogue
// of the real WinAPI call of the same name. It's a thin wrapper over the
// canonical export-table walk in GetExport (exports.go); use GetExport
// directly when you need forwarder resolution or ordinal information too.
func (p *PEFile) GetProcAddress(funcName string) (uint64, error) {
	exp, err := p.GetExport(funcName)
	if err != nil {
		return 0, err
	}
	if exp.RVA == 0 {
		return 0, fmt.Errorf("export %q is a forwarder (%s.%s), not a local RVA -- resolve it in the target DLL instead",
			funcName, exp.ForwardTarget, funcName)
	}
	return uint64(exp.RVA), nil
}
