#include "frida-gumjs.h"

#include "debug.h"
#include "cmplog.h"

#include "ctx.h"
#include "frida_cmplog.h"
#include "instrument.h"
#include "util.h"

#if defined(__aarch64__)

typedef struct {

  arm64_op_type type;
  uint8_t       size;

  union {

    arm64_op_mem mem;
    arm64_reg    reg;
    int64_t      imm;

  };

} cmplog_ctx_t;

typedef struct {

  cmplog_ctx_t operand1;
  cmplog_ctx_t operand2;
  size_t       size;

} cmplog_pair_ctx_t;

static gboolean cmplog_read_mem(GumCpuContext *ctx, uint8_t size,
                                arm64_op_mem *mem, gsize *val) {

  gsize base = 0;
  gsize index = 0;
  gsize address;

  if (mem->base != ARM64_REG_INVALID) { base = ctx_read_reg(ctx, mem->base); }

  if (mem->index != ARM64_REG_INVALID) {

    index = ctx_read_reg(ctx, mem->index);

  }

  address = base + index + mem->disp;

  if (!cmplog_is_readable(address, size)) { return FALSE; }

  switch (size) {

    case 1:
      *val = *((guint8 *)GSIZE_TO_POINTER(address));
      return TRUE;
    case 2:
      *val = *((guint16 *)GSIZE_TO_POINTER(address));
      return TRUE;
    case 4:
      *val = *((guint32 *)GSIZE_TO_POINTER(address));
      return TRUE;
    case 8:
      *val = *((guint64 *)GSIZE_TO_POINTER(address));
      return TRUE;
    default:
      FFATAL("Invalid operand size: %d\n", size);

  }

  return FALSE;

}

static gboolean cmplog_get_operand_value(GumCpuContext *context,
                                         cmplog_ctx_t *ctx, gsize *val) {

  switch (ctx->type) {

    case ARM64_OP_REG:
      *val = ctx_read_reg(context, ctx->reg);
      return TRUE;
    case ARM64_OP_IMM:
      *val = ctx->imm;
      return TRUE;
    case ARM64_OP_MEM:
      return cmplog_read_mem(context, ctx->size, &ctx->mem, val);
    default:
      FFATAL("Invalid operand type: %d\n", ctx->type);

  }

  return FALSE;

}

static void cmplog_call_callout(GumCpuContext *context, gpointer user_data) {

  UNUSED_PARAMETER(user_data);

  gsize address = context->pc;
  gsize x0 = ctx_read_reg(context, ARM64_REG_X0);
  gsize x1 = ctx_read_reg(context, ARM64_REG_X1);

  if (((G_MAXULONG - x0) < 31) || ((G_MAXULONG - x1) < 31)) return;

  if (!cmplog_is_readable(x0, 31) || !cmplog_is_readable(x1, 31)) return;

  void *ptr1 = GSIZE_TO_POINTER(x0);
  void *ptr2 = GSIZE_TO_POINTER(x1);

  guint64 k = instrument_get_offset_hash(GUM_ADDRESS(address));

  if (__afl_cmp_map->headers[k].type != CMP_TYPE_RTN) {

    __afl_cmp_map->headers[k].type = CMP_TYPE_RTN;
    __afl_cmp_map->headers[k].hits = 0;

  }

  u32 hits = 0;

  if (__afl_cmp_map->headers[k].hits == 0) {

    __afl_cmp_map->headers[k].shape = 30;

  } else {

    hits = __afl_cmp_map->headers[k].hits;

  }

  __afl_cmp_map->headers[k].hits = hits + 1;

  __afl_cmp_map->headers[k].shape = 30;

  hits &= CMP_MAP_RTN_H - 1;
  ((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v0_len = 31;
  ((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v1_len = 31;
  gum_memcpy(((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v0, ptr1,
             31);
  gum_memcpy(((struct cmpfn_operands *)__afl_cmp_map->log[k])[hits].v1, ptr2,
             31);

}

static void cmplog_instrument_put_operand(cmplog_ctx_t *ctx,
                                          cs_arm64_op  *operand) {

  ctx->type = operand->type;
  switch (operand->type) {

    case ARM64_OP_REG:
      gum_memcpy(&ctx->reg, &operand->reg, sizeof(arm64_reg));
      break;
    case ARM64_OP_IMM:
      gum_memcpy(&ctx->imm, &operand->imm, sizeof(int64_t));
      break;
    case ARM64_OP_MEM:
      gum_memcpy(&ctx->mem, &operand->mem, sizeof(arm64_op_mem));
      break;
    default:
      FFATAL("Invalid operand type: %d\n", operand->type);

  }

}

static void cmplog_instrument_call(const cs_insn      *instr,
                                   GumStalkerIterator *iterator) {

  cs_arm64     arm64 = instr->detail->arm64;
  cs_arm64_op *operand;

  switch (instr->id) {

    case ARM64_INS_BL:
    case ARM64_INS_BLR:
    case ARM64_INS_BLRAA:
    case ARM64_INS_BLRAAZ:
    case ARM64_INS_BLRAB:
    case ARM64_INS_BLRABZ:
      break;
    default:
      return;

  }

  if (arm64.op_count != 1) return;

  operand = &arm64.operands[0];

  if (operand->type == ARM64_OP_INVALID) return;

  gum_stalker_iterator_put_callout(iterator, cmplog_call_callout, NULL, NULL);

}

static void cmplog_handle_cmp_sub(GumCpuContext *context, gsize operand1,
                                  gsize operand2, uint8_t size) {

  gsize address = context->pc;

  register uintptr_t k = instrument_get_offset_hash(GUM_ADDRESS(address));

  if (__afl_cmp_map->headers[k].type != CMP_TYPE_INS)
    __afl_cmp_map->headers[k].hits = 0;

  u32 hits = 0;

  if (__afl_cmp_map->headers[k].hits == 0) {

    __afl_cmp_map->headers[k].type = CMP_TYPE_INS;
    __afl_cmp_map->headers[k].shape = (size - 1);

  } else {

    hits = __afl_cmp_map->headers[k].hits;

  }

  __afl_cmp_map->headers[k].hits = hits + 1;

  hits &= CMP_MAP_H - 1;
  __afl_cmp_map->log[k][hits].v0 = operand1;
  __afl_cmp_map->log[k][hits].v1 = operand2;

}

static void cmplog_cmp_sub_callout(GumCpuContext *context, gpointer user_data) {

  cmplog_pair_ctx_t *ctx = (cmplog_pair_ctx_t *)user_data;
  gsize              operand1;
  gsize              operand2;

  if (!cmplog_get_operand_value(context, &ctx->operand1, &operand1)) { return; }
  if (!cmplog_get_operand_value(context, &ctx->operand2, &operand2)) { return; }

  cmplog_handle_cmp_sub(context, operand1, operand2, ctx->size);

}

static void cmplog_instrument_cmp_sub_put_callout(GumStalkerIterator *iterator,
                                                  cs_arm64_op        *operand1,
                                                  cs_arm64_op        *operand2,
                                                  size_t              size) {

  cmplog_pair_ctx_t *ctx = g_malloc(sizeof(cmplog_pair_ctx_t));
  if (ctx == NULL) return;

  cmplog_instrument_put_operand(&ctx->operand1, operand1);
  cmplog_instrument_put_operand(&ctx->operand2, operand2);
  ctx->size = size;

  gum_stalker_iterator_put_callout(iterator, cmplog_cmp_sub_callout, ctx,
                                   g_free);

}

static void cmplog_instrument_cmp_sub(const cs_insn      *instr,
                                      GumStalkerIterator *iterator) {

  cs_arm64     arm64 = instr->detail->arm64;
  cs_arm64_op *operand1;
  cs_arm64_op *operand2;
  size_t       size;

  switch (instr->id) {

    case ARM64_INS_ADCS:
    case ARM64_INS_ADDS:
    case ARM64_INS_ANDS:
    case ARM64_INS_BICS:
    case ARM64_INS_CMN:
    case ARM64_INS_CMP:
    case ARM64_INS_CMPEQ:
    case ARM64_INS_CMPGE:
    case ARM64_INS_CMPGT:
    case ARM64_INS_CMPHI:
    case ARM64_INS_CMPHS:
    case ARM64_INS_CMPLE:
    case ARM64_INS_CMPLO:
    case ARM64_INS_CMPLS:
    case ARM64_INS_CMPLT:
    case ARM64_INS_CMPNE:
    case ARM64_INS_EORS:
    case ARM64_INS_NANDS:
    case ARM64_INS_NEGS:
    case ARM64_INS_NGCS:
    case ARM64_INS_NORS:
    case ARM64_INS_NOTS:
    case ARM64_INS_ORNS:
    case ARM64_INS_ORRS:
    case ARM64_INS_SBCS:
    case ARM64_INS_SUBS:
      break;

    default:
      return;

  }

  if (arm64.op_count != 2) return;

  operand1 = &arm64.operands[0];
  operand2 = &arm64.operands[1];

  if (operand1->type == ARM64_OP_INVALID) return;
  if (operand2->type == ARM64_OP_INVALID) return;

  size = ctx_get_size(instr, &arm64.operands[0]);

  cmplog_instrument_cmp_sub_put_callout(iterator, operand1, operand2, size);

}

void cmplog_instrument(const cs_insn *instr, GumStalkerIterator *iterator) {

  if (__afl_cmp_map == NULL) return;

  cmplog_instrument_call(instr, iterator);
  cmplog_instrument_cmp_sub(instr, iterator);

}

#endif

