/* Opcode printing code for the WebAssembly target
  Copyright (C) 2017-2024 Free Software Foundation, Inc.

  This file is part of libopcodes.

  This library is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 3 of the License, or
  (at your option) any later version.

  It is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
  License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston,
  MA 02110-1301, USA.  */

#include "sysdep.h"
#include "disassemble.h"
#include "opintl.h"
#include "safe-ctype.h"
#include "floatformat.h"
#include "libiberty.h"
#include "elf-bfd.h"
#include "elf/internal.h"
#include "elf/wasm32.h"
#include <stdint.h>

#include <limits.h>
#ifndef CHAR_BIT
#define CHAR_BIT 8
#endif

/* Type names for blocks and signatures.  */
#define BLOCK_TYPE_NONE              0x40
#define BLOCK_TYPE_I32               0x7f
#define BLOCK_TYPE_I64               0x7e
#define BLOCK_TYPE_F32               0x7d
#define BLOCK_TYPE_F64               0x7c

enum wasm_class
{
 wasm_typed,
 wasm_special,
 wasm_break,
 wasm_break_if,
 wasm_break_table,
 wasm_return,
 wasm_call,
 wasm_call_import,
 wasm_call_indirect,
 wasm_get_local,
 wasm_set_local,
 wasm_tee_local,
 wasm_drop,
 wasm_constant_i32,
 wasm_constant_i64,
 wasm_constant_f32,
 wasm_constant_f64,
 wasm_unary,
 wasm_binary,
 wasm_conv,
 wasm_load,
 wasm_store,
 wasm_select,
 wasm_relational,
 wasm_eqz,
 wasm_current_memory,
 wasm_grow_memory,
 wasm_signature
};

struct wasm32_private_data
{
 bool print_registers;
 bool print_well_known_globals;

 /* Limit valid symbols to those with a given prefix.  */
 const char *section_prefix;
};

typedef struct
{
 const char *name;
 const char *description;
} wasm32_options_t;

static const wasm32_options_t options[] =
{
 { "registers", N_("Disassemble \"register\" names") },
 { "globals",   N_("Name well-known globals") },
};

#define WASM_OPCODE(opcode, name, intype, outtype, clas, signedness)     \
 { name, wasm_ ## clas, opcode },

struct wasm32_opcode_s
{
 const char *name;
 enum wasm_class clas;
 unsigned char opcode;
} wasm32_opcodes[] =
{
#include "opcode/wasm.h"
 { NULL, 0, 0 }
};

/* Parse the disassembler options in OPTS and initialize INFO.  */

static void
parse_wasm32_disassembler_options (struct disassemble_info *info,
                                  const char *opts)
{
 struct wasm32_private_data *private = info->private_data;

 while (opts != NULL)
   {
     if (startswith (opts, "registers"))
       private->print_registers = true;
     else if (startswith (opts, "globals"))
       private->print_well_known_globals = true;

     opts = strchr (opts, ',');
     if (opts)
       opts++;
   }
}

/* Check whether SYM is valid.  Special-case absolute symbols, which
  are unhelpful to print, and arguments to a "call" insn, which we
  want to be in a section matching a given prefix.  */

static bool
wasm32_symbol_is_valid (asymbol *sym,
                       struct disassemble_info *info)
{
 struct wasm32_private_data *private_data = info->private_data;

 if (sym == NULL)
   return false;

 if (strcmp(sym->section->name, "*ABS*") == 0)
   return false;

 if (private_data && private_data->section_prefix != NULL
     && strncmp (sym->section->name, private_data->section_prefix,
                 strlen (private_data->section_prefix)))
   return false;

 return true;
}

/* Initialize the disassembler structures for INFO.  */

void
disassemble_init_wasm32 (struct disassemble_info *info)
{
 if (info->private_data == NULL)
   {
     static struct wasm32_private_data private;

     private.print_registers = false;
     private.print_well_known_globals = false;
     private.section_prefix = NULL;

     info->private_data = &private;
   }

 if (info->disassembler_options)
   {
     parse_wasm32_disassembler_options (info, info->disassembler_options);

     info->disassembler_options = NULL;
   }

 info->symbol_is_valid = wasm32_symbol_is_valid;
}

/* Read an LEB128-encoded integer from INFO at address PC, reading one
  byte at a time.  Set ERROR_RETURN if no complete integer could be
  read, LENGTH_RETURN to the number oof bytes read (including bytes
  in incomplete numbers).  SIGN means interpret the number as
  SLEB128.  Unfortunately, this is a duplicate of wasm-module.c's
  wasm_read_leb128 ().  */

static uint64_t
wasm_read_leb128 (bfd_vma pc,
                 struct disassemble_info *info,
                 bool *error_return,
                 unsigned int *length_return,
                 bool sign)
{
 uint64_t result = 0;
 unsigned int num_read = 0;
 unsigned int shift = 0;
 unsigned char byte = 0;
 unsigned char lost, mask;
 int status = 1;

 while (info->read_memory_func (pc + num_read, &byte, 1, info) == 0)
   {
     num_read++;

     if (shift < CHAR_BIT * sizeof (result))
       {
         result |= ((uint64_t) (byte & 0x7f)) << shift;
         /* These bits overflowed.  */
         lost = byte ^ (result >> shift);
         /* And this is the mask of possible overflow bits.  */
         mask = 0x7f ^ ((uint64_t) 0x7f << shift >> shift);
         shift += 7;
       }
     else
       {
         lost = byte;
         mask = 0x7f;
       }
     if ((lost & mask) != (sign && (int64_t) result < 0 ? mask : 0))
       status |= 2;

     if ((byte & 0x80) == 0)
       {
         status &= ~1;
         if (sign && shift < CHAR_BIT * sizeof (result) && (byte & 0x40))
           result |= -((uint64_t) 1 << shift);
         break;
       }
   }

 if (length_return != NULL)
   *length_return = num_read;
 if (error_return != NULL)
   *error_return = status != 0;

 return result;
}

/* Read a 32-bit IEEE float from PC using INFO, convert it to a host
  double, and store it at VALUE.  */

static int
read_f32 (double *value, bfd_vma pc, struct disassemble_info *info)
{
 bfd_byte buf[4];

 if (info->read_memory_func (pc, buf, sizeof (buf), info))
   return -1;

 floatformat_to_double (&floatformat_ieee_single_little, buf,
                        value);

 return sizeof (buf);
}

/* Read a 64-bit IEEE float from PC using INFO, convert it to a host
  double, and store it at VALUE.  */

static int
read_f64 (double *value, bfd_vma pc, struct disassemble_info *info)
{
 bfd_byte buf[8];

 if (info->read_memory_func (pc, buf, sizeof (buf), info))
   return -1;

 floatformat_to_double (&floatformat_ieee_double_little, buf,
                        value);

 return sizeof (buf);
}

/* Main disassembly routine.  Disassemble insn at PC using INFO.  */

int
print_insn_wasm32 (bfd_vma pc, struct disassemble_info *info)
{
 unsigned char opcode;
 struct wasm32_opcode_s *op;
 bfd_byte buffer[16];
 void *stream = info->stream;
 fprintf_ftype prin = info->fprintf_func;
 struct wasm32_private_data *private_data = info->private_data;
 uint64_t val;
 int len;
 unsigned int bytes_read;
 bool error;

 if (info->read_memory_func (pc, buffer, 1, info))
   return -1;

 opcode = buffer[0];

 for (op = wasm32_opcodes; op->name; op++)
   if (op->opcode == opcode)
     break;

 if (!op->name)
   {
     prin (stream, "\t.byte 0x%02x\n", buffer[0]);
     return 1;
   }

 len = 1;

 prin (stream, "\t");
 prin (stream, "%s", op->name);

 if (op->clas == wasm_typed)
   {
     val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, false);
     if (error)
       return -1;
     len += bytes_read;
     switch (val)
       {
       case BLOCK_TYPE_NONE:
         prin (stream, "[]");
         break;
       case BLOCK_TYPE_I32:
         prin (stream, "[i]");
         break;
       case BLOCK_TYPE_I64:
         prin (stream, "[l]");
         break;
       case BLOCK_TYPE_F32:
         prin (stream, "[f]");
         break;
       case BLOCK_TYPE_F64:
         prin (stream, "[d]");
         break;
       default:
         return -1;
       }
   }

 switch (op->clas)
   {
   case wasm_special:
   case wasm_eqz:
   case wasm_binary:
   case wasm_unary:
   case wasm_conv:
   case wasm_relational:
   case wasm_drop:
   case wasm_signature:
   case wasm_call_import:
   case wasm_typed:
   case wasm_select:
     break;

   case wasm_break_table:
     {
       uint32_t target_count, i;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       target_count = val;
       if (error || target_count != val || target_count == (uint32_t) -1)
         return -1;
       len += bytes_read;
       prin (stream, " %u", target_count);
       for (i = 0; i < target_count + 1; i++)
         {
           uint32_t target;
           val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                                   false);
           target = val;
           if (error || target != val)
             return -1;
           len += bytes_read;
           prin (stream, " %u", target);
         }
     }
     break;

   case wasm_break:
   case wasm_break_if:
     {
       uint32_t depth;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       depth = val;
       if (error || depth != val)
         return -1;
       len += bytes_read;
       prin (stream, " %u", depth);
     }
     break;

   case wasm_return:
     break;

   case wasm_constant_i32:
   case wasm_constant_i64:
     val = wasm_read_leb128 (pc + len, info, &error, &bytes_read, true);
     if (error)
       return -1;
     len += bytes_read;
     prin (stream, " %" PRId64, val);
     break;

   case wasm_constant_f32:
     {
       double fconstant;
       int ret;
       /* This appears to be the best we can do, even though we're
          using host doubles for WebAssembly floats.  */
       ret = read_f32 (&fconstant, pc + len, info);
       if (ret < 0)
         return -1;
       len += ret;
       prin (stream, " %.9g", fconstant);
     }
     break;

   case wasm_constant_f64:
     {
       double fconstant;
       int ret;
       ret = read_f64 (&fconstant, pc + len, info);
       if (ret < 0)
         return -1;
       len += ret;
       prin (stream, " %.17g", fconstant);
     }
     break;

   case wasm_call:
     {
       uint32_t function_index;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       function_index = val;
       if (error || function_index != val)
         return -1;
       len += bytes_read;
       prin (stream, " ");
       private_data->section_prefix = ".space.function_index";
       (*info->print_address_func) ((bfd_vma) function_index, info);
       private_data->section_prefix = NULL;
     }
     break;

   case wasm_call_indirect:
     {
       uint32_t type_index, xtra_index;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       type_index = val;
       if (error || type_index != val)
         return -1;
       len += bytes_read;
       prin (stream, " %u", type_index);
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       xtra_index = val;
       if (error || xtra_index != val)
         return -1;
       len += bytes_read;
       prin (stream, " %u", xtra_index);
     }
     break;

   case wasm_get_local:
   case wasm_set_local:
   case wasm_tee_local:
     {
       uint32_t local_index;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       local_index = val;
       if (error || local_index != val)
         return -1;
       len += bytes_read;
       prin (stream, " %u", local_index);
       if (strcmp (op->name + 4, "local") == 0)
         {
           static const char *locals[] =
             {
               "$dpc", "$sp1", "$r0", "$r1", "$rpc", "$pc0",
               "$rp", "$fp", "$sp",
               "$r2", "$r3", "$r4", "$r5", "$r6", "$r7",
               "$i0", "$i1", "$i2", "$i3", "$i4", "$i5", "$i6", "$i7",
               "$f0", "$f1", "$f2", "$f3", "$f4", "$f5", "$f6", "$f7",
             };
           if (private_data->print_registers
               && local_index < ARRAY_SIZE (locals))
             prin (stream, " <%s>", locals[local_index]);
         }
       else
         {
           static const char *globals[] =
             {
               "$got", "$plt", "$gpo"
             };
           if (private_data->print_well_known_globals
               && local_index < ARRAY_SIZE (globals))
             prin (stream, " <%s>", globals[local_index]);
         }
     }
     break;

   case wasm_grow_memory:
   case wasm_current_memory:
     {
       uint32_t reserved_size;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       reserved_size = val;
       if (error || reserved_size != val)
         return -1;
       len += bytes_read;
       prin (stream, " %u", reserved_size);
     }
     break;

   case wasm_load:
   case wasm_store:
     {
       uint32_t flags, offset;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       flags = val;
       if (error || flags != val)
         return -1;
       len += bytes_read;
       val = wasm_read_leb128 (pc + len, info, &error, &bytes_read,
                               false);
       offset = val;
       if (error || offset != val)
         return -1;
       len += bytes_read;
       prin (stream, " a=%u %u", flags, offset);
     }
     break;
   }
 return len;
}

/* Print valid disassembler options to STREAM.  */

void
print_wasm32_disassembler_options (FILE *stream)
{
 unsigned int i, max_len = 0;

 fprintf (stream, _("\
The following WebAssembly-specific disassembler options are supported for use\n\
with the -M switch:\n"));

 for (i = 0; i < ARRAY_SIZE (options); i++)
   {
     unsigned int len = strlen (options[i].name);

     if (max_len < len)
       max_len = len;
   }

 for (i = 0, max_len++; i < ARRAY_SIZE (options); i++)
   fprintf (stream, "  %s%*c %s\n",
            options[i].name,
            (int)(max_len - strlen (options[i].name)), ' ',
            _(options[i].description));
}