// radare - LGPL - Copyright 2026 - pancake

#include <r_anal.h>
#include <r_anal_priv.h>

#define SENT ((ut64)0xdeadbeefcafebabeULL)
#define MAX_REGS 8
#define MAX_FCN_BBS 256
#define DEFAULT_NARGS 4

typedef enum { OUT_PLAIN, OUT_QUIET, OUT_JSON, OUT_STAR } OutMode;

static OutMode parse_mode(const char **p) {
	switch (**p) {
	case 'j': (*p)++; return OUT_JSON;
	case '*': (*p)++; return OUT_STAR;
	case 'q': (*p)++; return OUT_QUIET;
	}
	return OUT_PLAIN;
}

static RCoreHelpMessage help = {
	"Usage:", "a:callargs", "[-qj*f] [addr] # analyze call args via ESIL",
	"a:callargs", " [addr]", "plaintext summary of args at call [addr]",
	"a:callargs-q", " [addr]", "quiet plaintext (stripped symbol, no arg names or raw)",
	"a:callargs-j", " [addr]", "same in JSON",
	"a:callargs-*", " [addr]", "emit a CCu comment command for the call",
	"a:callargs-f", "", "apply to every call in the current function",
	"a:callargs-fq", "", "same as 'f' but quiet",
	"a:callargs-fj", "", "same as 'f' but as a JSON array",
	"a:callargs-f*", "", "same as 'f' but as r2 commands",
	NULL
};

static ut64 normalize(ut64 v) {
	return ((v >> 32) == 0xffffffffULL && (v & 0x80000000ULL)) ? (v & 0xffffffffULL) : v;
}

static bool is_call(int t) {
	return t == R_ANAL_OP_TYPE_CALL || t == R_ANAL_OP_TYPE_UCALL
		|| t == R_ANAL_OP_TYPE_ICALL || t == R_ANAL_OP_TYPE_RCALL
		|| t == R_ANAL_OP_TYPE_CCALL || t == R_ANAL_OP_TYPE_UCCALL;
}

static void prime_regs(RAnal *a) {
	a->coreb.cmd (a->coreb.core, "aei");
	a->coreb.cmd (a->coreb.core, "aeim");
	ut64 sp = r_reg_getv (a->reg, "SP");
	r_reg_arena_zero (a->reg);
	if (sp && sp != UT64_MAX) {
		r_reg_setv (a->reg, "SP", sp);
	}
	char rn[16];
	int i;
	for (i = 0; i < MAX_REGS; i++) {
		snprintf (rn, sizeof (rn), "A%d", i);
		r_reg_setv (a->reg, rn, SENT);
	}
}

static void emulate_to(RAnal *a, ut64 pcv) {
	RAnalFunction *fcn = r_anal_get_fcn_in (a, pcv, -1);
	if (fcn && r_list_length (fcn->bbs) <= MAX_FCN_BBS) {
		a->coreb.cmdf (a->coreb.core, "abpe 0x%08"PFMT64x, pcv);
		return;
	}
	RList *bbs = r_anal_get_blocks_in (a, pcv);
	RAnalBlock *bb = bbs ? (RAnalBlock *)r_list_first (bbs) : NULL;
	if (bb) {
		a->coreb.cmdf (a->coreb.core, "aepc 0x%08"PFMT64x, bb->addr);
		a->coreb.cmdf (a->coreb.core, "aesou 0x%08"PFMT64x, pcv);
	}
	r_list_free (bbs);
}

static RAnalOp *decode(RAnal *a, ut64 pcv) {
	ut8 buf[16] = {0};
	if (!a->iob.read_at || a->iob.read_at (a->iob.io, pcv, buf, sizeof (buf)) != sizeof (buf)) {
		return NULL;
	}
	RAnalOp *op = r_anal_op_new ();
	if (r_anal_op (a, op, pcv, buf, sizeof (buf), R_ARCH_OP_MASK_BASIC) <= 0) {
		r_anal_op_free (op);
		return NULL;
	}
	return op;
}

static const char *resolve_callee(RAnal *a, ut64 target) {
	if (!target || target == UT64_MAX) {
		return NULL;
	}
	RAnalFunction *fcn = r_anal_get_function_at (a, target);
	if (fcn) {
		return fcn->name;
	}
	RFlagItem *item = a->flb.get_at ? a->flb.get_at (a->flb.f, target, true) : NULL;
	return item ? item->name : NULL;
}

static void emit_arg(RAnal *a, int idx, const char *name, const char *type,
		const char *source, const char *fmt, int size, ut64 raw,
		bool on_stack, bool quiet, PJ *pj, RStrBuf *sb) {
	bool unknown = (raw == SENT);
	ut64 disp = normalize (raw);
	char *typed = NULL;
	if (!unknown && fmt && name) {
		// quiet pf never prints the field name, keep it and fmt away from the command parser
		char *cmd = r_str_newf ("pfq %s%s", on_stack ? "*" : "", fmt);
		typed = a->coreb.callAt (a->coreb.core, disp, cmd);
		free (cmd);
		if (typed) {
			r_str_trim (typed);
			if (!*typed) {
				R_FREE (typed);
			}
		}
	}
	if (pj) {
		pj_o (pj);
		pj_kn (pj, "index", idx);
		if (name) { pj_ks (pj, "name", name); }
		if (type) { pj_ks (pj, "type", type); }
		if (source) { pj_ks (pj, "source", source); }
		if (size) { pj_kn (pj, "size", size); }
		pj_kb (pj, "known", !unknown);
		if (!unknown) {
			pj_kn (pj, "raw", disp);
			if (typed) { pj_ks (pj, "value", typed); }
		}
		pj_end (pj);
	} else {
		if (idx > 0) { r_strbuf_append (sb, ", "); }
		if (!quiet && name) { r_strbuf_appendf (sb, "%s=", name); }
		if (unknown) {
			r_strbuf_append (sb, "?");
		} else if (typed) {
			if (quiet) {
				r_strbuf_append (sb, typed);
			} else {
				r_strbuf_appendf (sb, "%s /*0x%"PFMT64x"*/", typed, disp);
			}
		} else {
			r_strbuf_appendf (sb, "0x%"PFMT64x, disp);
		}
	}
	free (typed);
}

// the declared type with any const qualifier stripped, as the type.* sdb keys spell it
static const char *callargs_ctype(const char *type) {
	return (type && r_str_startswith (type, "const "))? type + 6: type;
}

typedef struct {
	const char *reg; // register home, NULL when the arg is on the stack or unresolved
	st64 off; // offset from SP, -1 when the arg is not on the stack
	int size; // declared type size in bytes, 0 when unknown
} CallArgPlace;

// where each arg sits: a type wider than a word shifts the tail slots after it, explicit ^N slots state their own
static CallArgPlace *callargs_places(RAnal *a, const char *cc, const char *key, int nargs, bool rev) {
	CallArgPlace *places = R_NEWS0 (CallArgPlace, nargs);
	if (!places) {
		return NULL;
	}
	int i;
	for (i = 0; i < nargs; i++) {
		char *type = r_type_func_args_type (a->sdb_types, key, i);
		const char *ctype = callargs_ctype (type);
		places[i].size = ctype? (int)(sdb_num_getf (a->sdb_types, NULL, "type.%s.size", ctype) / 8): 0;
		places[i].off = -1;
		free (type);
	}
	st64 acc = -1;
	for (i = 0; i < nargs; i++) {
		// walk in stack address order, as a reverse convention pushes the last arg at SP
		const int n = rev? nargs - 1 - i: i;
		RAnalCCArgSlot slot;
		// emulation stops at the call, so the first stack arg sits at SP with no return address slot
		if (!r_anal_cc_argslot (a, cc, n, nargs, false, &slot)) {
			continue;
		}
		if (slot.reg) {
			places[n].reg = slot.reg;
			continue;
		}
		const int span = R_MAX (places[n].size, slot.size);
		if (slot.fixed) {
			places[n].off = slot.off;
			acc = R_MAX (acc, slot.off + span);
			continue;
		}
		places[n].off = R_MAX (acc, slot.off);
		acc = places[n].off + span;
	}
	return places;
}

static int emit_signature(RAnal *a, const char *fcn_name, bool quiet, PJ *pj, RStrBuf *sb) {
	if (!fcn_name) {
		return -1;
	}
	char *key = r_type_func_name (a->sdb_types, fcn_name);
	if (!key) {
		return -1;
	}
	const char *cc = r_anal_cc_func (a, key);
	if (!cc) {
		cc = r_anal_cc_default (a);
	}
	int nargs;
	if (!cc || !r_type_func_args_count (a->sdb_types, key, &nargs) || nargs <= 0) {
		free (key);
		return -1;
	}
	CallArgPlace *places = callargs_places (a, cc, key, nargs, r_anal_cc_stack_rev (a, cc));
	if (!places) {
		free (key);
		return -1;
	}
	int i;
	for (i = 0; i < nargs; i++) {
		const char *name = r_type_func_args_name (a->sdb_types, key, i);
		char *type = r_type_func_args_type (a->sdb_types, key, i);
		const char *ctype = callargs_ctype (type);
		const char *fmt = ctype? sdb_const_getf (a->sdb_types, NULL, "type.%s", ctype): NULL;
		const char *src = r_anal_cc_argloc (a, cc, i, 0, nargs);
		const bool on_stack = places[i].off >= 0;
		ut64 raw = SENT;
		if (places[i].reg) {
			raw = r_reg_getv (a->reg, places[i].reg);
		} else if (on_stack) {
			raw = r_reg_getv (a->reg, "SP") + places[i].off;
		}
		emit_arg (a, i, name, type, src, fmt, places[i].size, raw, on_stack, quiet, pj, sb);
		free (type);
	}
	free (places);
	free (key);
	return nargs;
}

static void emit_raw(RAnal *a, ut64 target, bool quiet, PJ *pj, RStrBuf *sb) {
	int nargs = DEFAULT_NARGS;
	RAnalFunction *callee = (target && target != UT64_MAX) ? r_anal_get_function_at (a, target) : NULL;
	if (callee) {
		int n = r_anal_var_count_args (callee);
		if (n > 0) {
			nargs = n;
		}
	}
	nargs = R_MIN (nargs, MAX_REGS);
	int i;
	char rn[16];
	for (i = 0; i < nargs; i++) {
		snprintf (rn, sizeof (rn), "A%d", i);
		emit_arg (a, i, rn, NULL, rn, NULL, 0, r_reg_getv (a->reg, rn), false, quiet, pj, sb);
	}
}

static char *analyze_call(RAnal *a, ut64 pcv, OutMode mode) {
	prime_regs (a);
	emulate_to (a, pcv);
	RAnalOp *op = decode (a, pcv);
	if (!op || !is_call (op->type)) {
		r_anal_op_free (op);
		return strdup ("");
	}
	const char *fcn_name = resolve_callee (a, op->jump);
	if (!fcn_name) {
		fcn_name = r_anal_call_type_at (a, pcv);
	}
	char *key = fcn_name ? r_type_func_name (a->sdb_types, fcn_name) : NULL;
	const char *ret_type = key ? r_type_func_ret (a->sdb_types, key) : NULL;
	const char *cc = key ? r_anal_cc_func (a, key) : NULL;
	if (!cc) {
		cc = r_anal_cc_default (a);
	}
	bool quiet = (mode == OUT_QUIET);
	const char *disp_name = quiet && key ? key : fcn_name;
	PJ *pj = NULL;
	RStrBuf *sb = NULL;
	if (mode == OUT_JSON) {
		pj = pj_new ();
		pj_o (pj);
		pj_kn (pj, "addr", pcv);
		if (op->jump != UT64_MAX) { pj_kn (pj, "target", op->jump); }
		if (fcn_name) { pj_ks (pj, "function", fcn_name); }
		if (ret_type) { pj_ks (pj, "return_type", ret_type); }
		if (cc) { pj_ks (pj, "calling_convention", cc); }
		pj_ka (pj, "args");
	} else {
		sb = r_strbuf_new ("");
		if (disp_name && ret_type) {
			r_strbuf_appendf (sb, "%s%s%s(", ret_type,
				ret_type[strlen (ret_type) - 1] == '*' ? "" : " ", disp_name);
		} else if (disp_name) {
			r_strbuf_appendf (sb, "%s(", disp_name);
		} else if (op->jump != UT64_MAX && op->jump) {
			r_strbuf_appendf (sb, "0x%08"PFMT64x"(", op->jump);
		} else {
			r_strbuf_append (sb, "?(");
		}
	}
	free (key);
	if (emit_signature (a, fcn_name, quiet, pj, sb) < 0) {
		emit_raw (a, op->jump, quiet, pj, sb);
	}
	r_anal_op_free (op);
	if (mode == OUT_JSON) {
		pj_end (pj);
		pj_end (pj);
		return pj_drain (pj);
	}
	r_strbuf_append (sb, ")");
	char *s = r_strbuf_drain (sb);
	if (mode == OUT_STAR) {
		char *u = (char *)r_base64_encode_dyn ((const ut8 *)s, -1);
		char *ret = r_str_newf ("'@0x%08"PFMT64x"'CCu base64:%s", pcv, u ? u : "");
		free (s);
		free (u);
		return ret;
	}
	return s;
}

static char *analyze_fcn(RAnal *a, OutMode mode) {
	ut64 here = a->coreb.numGet (a->coreb.core, "$$");
	RAnalFunction *fcn = r_anal_get_fcn_in (a, here, 0);
	if (!fcn) {
		R_LOG_ERROR ("No function at 0x%08"PFMT64x, here);
		return strdup ("");
	}
	RStrBuf *out = r_strbuf_new (mode == OUT_JSON ? "[" : "");
	RListIter *it;
	RAnalBlock *bb;
	bool first = true;
	r_list_foreach (fcn->bbs, it, bb) {
		ut64 bb_end = bb->addr + bb->size;
		ut64 at = bb->addr;
		while (at < bb_end) {
			RAnalOp *op = decode (a, at);
			if (!op || op->size < 1) {
				r_anal_op_free (op);
				break;
			}
			int sz = op->size;
			bool call = is_call (op->type);
			r_anal_op_free (op);
			if (call) {
				char *s = analyze_call (a, at, mode);
				if (s && *s) {
					if (mode == OUT_JSON) {
						if (!first) { r_strbuf_append (out, ","); }
						r_strbuf_append (out, s);
					} else {
						r_strbuf_append (out, s);
						r_strbuf_append (out, "\n");
					}
					first = false;
				}
				free (s);
			}
			at += sz;
		}
	}
	if (mode == OUT_JSON) {
		r_strbuf_append (out, "]");
	}
	return r_strbuf_drain (out);
}

static char *callargscmd(RAnal *a, const char *input) {
	if (!r_str_startswith (input, "callargs")) {
		return NULL;
	}
	const char *arg = input + 8;
	if (*arg == '?' || (!*arg && !a->coreb.numGet)) {
		if (a->coreb.help) {
			a->coreb.help (a->coreb.core, help);
		}
		return strdup ("");
	}
	OutMode mode = OUT_PLAIN;
	if (*arg == '-') {
		arg++;
		if (*arg == 'f') {
			arg++;
			return analyze_fcn (a, parse_mode (&arg));
		}
		mode = parse_mode (&arg);
	}
	while (*arg == ' ') { arg++; }
	ut64 pcv = a->coreb.numGet (a->coreb.core, R_STR_ISEMPTY (arg) ? "$$" : arg);
	if (!pcv || pcv == UT64_MAX) {
		if (a->coreb.help) {
			a->coreb.help (a->coreb.core, help);
		}
		return strdup ("");
	}
	return analyze_call (a, pcv, mode);
}

RAnalPlugin r_anal_plugin_callargs = {
	.meta = {
		.name = "callargs",
		.desc = "analyze function call arguments via ESIL emulation",
		.license = "LGPL-3.0-only",
		.author = "pancake",
	},
	.cmd = callargscmd,
};

#ifndef R2_PLUGIN_INCORE
R_API RLibStruct radare_plugin = {
	.type = R_LIB_TYPE_ANAL,
	.data = &r_anal_plugin_callargs,
	.version = R2_VERSION
};
#endif
