//===----------------------------- unwind-pe.h ----------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
// Pointer-Encoding decoder. Derived from:
//   - libcxxabi/src/Unwind/dwarf2.h
//   - libcxxabi/src/Unwind/AddressSpace.h
//
//===----------------------------------------------------------------------===//

#ifndef UNWIND_PE_H
#define UNWIND_PE_H

#include <assert.h>
#include <stdint.h>
#include <string.h>

// FSF exception handling Pointer-Encoding constants
// Used in CFI augmentation by GCC
enum {
  DW_EH_PE_ptr       = 0x00,
  DW_EH_PE_uleb128   = 0x01,
  DW_EH_PE_udata2    = 0x02,
  DW_EH_PE_udata4    = 0x03,
  DW_EH_PE_udata8    = 0x04,
  DW_EH_PE_signed    = 0x08,
  DW_EH_PE_sleb128   = 0x09,
  DW_EH_PE_sdata2    = 0x0A,
  DW_EH_PE_sdata4    = 0x0B,
  DW_EH_PE_sdata8    = 0x0C,
  DW_EH_PE_absptr    = 0x00,
  DW_EH_PE_pcrel     = 0x10,
  DW_EH_PE_textrel   = 0x20,
  DW_EH_PE_datarel   = 0x30,
  DW_EH_PE_funcrel   = 0x40,
  DW_EH_PE_aligned   = 0x50,
  DW_EH_PE_indirect  = 0x80,
  DW_EH_PE_omit      = 0xFF
};

/// Read a ULEB128 into a 64-bit word.
static uint64_t unw_getULEB128(uintptr_t *addr) {
  const uint8_t *p = (uint8_t *)*addr;
  uint64_t result = 0;
  int bit = 0;
  do {
    uint64_t b;

    b = *p & 0x7f;

    if (bit >= 64 || b << bit >> bit != b) {
      assert(!"malformed uleb128 expression");
    } else {
      result |= b << bit;
      bit += 7;
    }
  } while (*p++ >= 0x80);
  *addr = (uintptr_t) p;
  return result;
}

/// Read a SLEB128 into a 64-bit word.
static int64_t unw_getSLEB128(uintptr_t *addr) {
  const uint8_t *p = (uint8_t *)addr;
  int64_t result = 0;
  int bit = 0;
  uint8_t byte;
  do {
    byte = *p++;
    result |= ((byte & 0x7f) << bit);
    bit += 7;
  } while (byte & 0x80);
  // sign extend negative numbers
  if ((byte & 0x40) != 0)
    result |= (-1LL) << bit;
  *addr = (uintptr_t) p;
  return result;
}

static uint16_t unw_get16(uintptr_t addr) {
  uint16_t val;
  memcpy(&val, (void *)addr, sizeof(val));
  return val;
}

static uint32_t unw_get32(uintptr_t addr) {
  uint32_t val;
  memcpy(&val, (void *)addr, sizeof(val));
  return val;
}

static uint64_t unw_get64(uintptr_t addr) {
  uint64_t val;
  memcpy(&val, (void *)addr, sizeof(val));
  return val;
}

static uintptr_t unw_getP(uintptr_t addr) {
  if (sizeof(uintptr_t) == 8)
    return unw_get64(addr);
  else
    return unw_get32(addr);
}

static const unsigned char *read_uleb128(const unsigned char *p,
                                         _uleb128_t *ret) {
  uintptr_t addr = (uintptr_t)p;
  *ret = unw_getULEB128(&addr);
  return (unsigned char *)addr;
}

static const unsigned char *read_encoded_value(struct _Unwind_Context *ctx,
                                               unsigned char encoding,
                                               const unsigned char *p,
                                               _Unwind_Ptr *ret) {
  uintptr_t addr = (uintptr_t)p;
  uintptr_t startAddr = addr;
  uintptr_t result;

  (void)ctx;

  // first get value
  switch (encoding & 0x0F) {
  case DW_EH_PE_ptr:
    result = unw_getP(addr);
    p += sizeof(uintptr_t);
    break;
  case DW_EH_PE_uleb128:
    result = (uintptr_t)unw_getULEB128(&addr);
    p = (const unsigned char *)addr;
    break;
  case DW_EH_PE_udata2:
    result = unw_get16(addr);
    p += 2;
    break;
  case DW_EH_PE_udata4:
    result = unw_get32(addr);
    p += 4;
    break;
  case DW_EH_PE_udata8:
    result = (uintptr_t)unw_get64(addr);
    p += 8;
    break;
  case DW_EH_PE_sleb128:
    result = (uintptr_t)unw_getSLEB128(&addr);
    p = (const unsigned char *)addr;
    break;
  case DW_EH_PE_sdata2:
    // Sign extend from signed 16-bit value.
    result = (uintptr_t)(int16_t)unw_get16(addr);
    p += 2;
    break;
  case DW_EH_PE_sdata4:
    // Sign extend from signed 32-bit value.
    result = (uintptr_t)(int32_t)unw_get32(addr);
    p += 4;
    break;
  case DW_EH_PE_sdata8:
    result = (uintptr_t)unw_get64(addr);
    p += 8;
    break;
  default:
    assert(!"unknown pointer encoding");
  }

  // then add relative offset
  switch (encoding & 0x70) {
  case DW_EH_PE_absptr:
    // do nothing
    break;
  case DW_EH_PE_pcrel:
    result += startAddr;
    break;
  case DW_EH_PE_textrel:
    assert(!"DW_EH_PE_textrel pointer encoding not supported");
    break;
  case DW_EH_PE_datarel:
    assert(!"DW_EH_PE_datarel pointer encoding not supported");
    break;
  case DW_EH_PE_funcrel:
    assert(!"DW_EH_PE_funcrel pointer encoding not supported");
    break;
  case DW_EH_PE_aligned:
    assert(!"DW_EH_PE_aligned pointer encoding not supported");
    break;
  default:
    assert(!"unknown pointer encoding");
    break;
  }

  if (encoding & DW_EH_PE_indirect)
    result = unw_getP(result);

  *ret = result;
  return p;
}

#endif  // UNWIND_PE_H
