/* codeview.c - CodeView debug support
  Copyright (C) 2022-2024 Free Software Foundation, Inc.

  This file is part of GAS, the GNU Assembler.

  GAS 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, or (at your option)
  any later version.

  GAS 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 GAS; see the file COPYING.  If not, write to the Free
  Software Foundation, 51 Franklin Street - Fifth Floor, Boston, MA
  02110-1301, USA.  */

#include "as.h"
#include "codeview.h"
#include "subsegs.h"
#include "filenames.h"
#include "md5.h"

#if defined (TE_PE) && defined (O_secrel)

#define NUM_MD5_BYTES           16

#define FILE_ENTRY_PADDING      2
#define FILE_ENTRY_LENGTH       (sizeof (struct file_checksum) + NUM_MD5_BYTES \
                                + FILE_ENTRY_PADDING)

struct line
{
 struct line *next;
 unsigned int lineno;
 addressT frag_offset;
};

struct line_file
{
 struct line_file *next;
 unsigned int fileno;
 struct line *lines_head, *lines_tail;
 unsigned int num_lines;
};

struct line_block
{
 struct line_block *next;
 segT seg;
 unsigned int subseg;
 fragS *frag;
 symbolS *sym;
 struct line_file *files_head, *files_tail;
};

struct source_file
{
 struct source_file *next;
 unsigned int num;
 char *filename;
 uint32_t string_pos;
 uint8_t md5[NUM_MD5_BYTES];
};

static struct line_block *blocks_head = NULL, *blocks_tail = NULL;
static struct source_file *files_head = NULL, *files_tail = NULL;
static unsigned int num_source_files = 0;

/* Return the size of the current fragment (taken from dwarf2dbg.c).  */
static offsetT
get_frag_fix (fragS *frag, segT seg)
{
 frchainS *fr;

 if (frag->fr_next)
   return frag->fr_fix;

 for (fr = seg_info (seg)->frchainP; fr; fr = fr->frch_next)
   if (fr->frch_last == frag)
     return (char *) obstack_next_free (&fr->frch_obstack) - frag->fr_literal;

 abort ();
}

/* Emit a .secrel32 relocation.  */
static void
emit_secrel32_reloc (symbolS *sym)
{
 expressionS exp;

 memset (&exp, 0, sizeof (exp));
 exp.X_op = O_secrel;
 exp.X_add_symbol = sym;
 exp.X_add_number = 0;
 emit_expr (&exp, sizeof (uint32_t));
}

/* Emit a .secidx relocation.  */
static void
emit_secidx_reloc (symbolS *sym)
{
 expressionS exp;

 memset (&exp, 0, sizeof (exp));
 exp.X_op = O_secidx;
 exp.X_add_symbol = sym;
 exp.X_add_number = 0;
 emit_expr (&exp, sizeof (uint16_t));
}

/* Write the DEBUG_S_STRINGTABLE subsection.  */
static void
write_string_table (void)
{
 uint32_t len;
 unsigned int padding;
 char *ptr, *start;

 len = 1;

 for (struct source_file *sf = files_head; sf; sf = sf->next)
   {
     len += strlen (sf->filename) + 1;
   }

 if (len % 4)
   padding = 4 - (len % 4);
 else
   padding = 0;

 ptr = frag_more (sizeof (uint32_t) + sizeof (uint32_t) + len + padding);

 bfd_putl32 (DEBUG_S_STRINGTABLE, ptr);
 ptr += sizeof (uint32_t);
 bfd_putl32 (len, ptr);
 ptr += sizeof (uint32_t);

 start = ptr;

 *ptr = 0;
 ptr++;

 for (struct source_file *sf = files_head; sf; sf = sf->next)
   {
     size_t fn_len = strlen (sf->filename);

     sf->string_pos = ptr - start;

     memcpy(ptr, sf->filename, fn_len + 1);
     ptr += fn_len + 1;
   }

 memset (ptr, 0, padding);
}

/* Write the DEBUG_S_FILECHKSMS subsection.  */
static void
write_checksums (void)
{
 uint32_t len;
 char *ptr;

 len = FILE_ENTRY_LENGTH * num_source_files;

 ptr = frag_more (sizeof (uint32_t) + sizeof (uint32_t) + len);

 bfd_putl32 (DEBUG_S_FILECHKSMS, ptr);
 ptr += sizeof (uint32_t);
 bfd_putl32 (len, ptr);
 ptr += sizeof (uint32_t);

 for (struct source_file *sf = files_head; sf; sf = sf->next)
   {
     struct file_checksum fc;

     fc.file_id = sf->string_pos;
     fc.checksum_length = NUM_MD5_BYTES;
     fc.checksum_type = CHKSUM_TYPE_MD5;

     memcpy (ptr, &fc, sizeof (struct file_checksum));
     ptr += sizeof (struct file_checksum);

     memcpy (ptr, sf->md5, NUM_MD5_BYTES);
     ptr += NUM_MD5_BYTES;

     memset (ptr, 0, FILE_ENTRY_PADDING);
     ptr += FILE_ENTRY_PADDING;
   }
}

/* Write the DEBUG_S_LINES subsection.  */
static void
write_lines_info (void)
{
 while (blocks_head)
   {
     struct line_block *lb;
     struct line_file *lf;
     uint32_t len;
     uint32_t off;
     char *ptr;

     lb = blocks_head;

     bfd_putl32 (DEBUG_S_LINES, frag_more (sizeof (uint32_t)));

     len = sizeof (struct cv_lines_header);

     for (lf = lb->files_head; lf; lf = lf->next)
       {
         len += sizeof (struct cv_lines_block);
         len += sizeof (struct cv_line) * lf->num_lines;
       }

     bfd_putl32 (len, frag_more (sizeof (uint32_t)));

     /* Write the header (struct cv_lines_header).  We can't use a struct
        for this as we're also emitting relocations.  */

     emit_secrel32_reloc (lb->sym);
     emit_secidx_reloc (lb->sym);

     ptr = frag_more (len - sizeof (uint32_t) - sizeof (uint16_t));

     /* Flags */
     bfd_putl16 (0, ptr);
     ptr += sizeof (uint16_t);

     off = lb->files_head->lines_head->frag_offset;

     /* Length of region */
     bfd_putl32 (get_frag_fix (lb->frag, lb->seg) - off, ptr);
     ptr += sizeof (uint32_t);

     while (lb->files_head)
       {
         struct cv_lines_block *block = (struct cv_lines_block *) ptr;

         lf = lb->files_head;

         bfd_putl32(lf->fileno * FILE_ENTRY_LENGTH, &block->file_id);
         bfd_putl32(lf->num_lines, &block->num_lines);
         bfd_putl32(sizeof (struct cv_lines_block)
                    + (sizeof (struct cv_line) * lf->num_lines),
                    &block->length);

         ptr += sizeof (struct cv_lines_block);

         while (lf->lines_head)
           {
             struct line *l;
             struct cv_line *l2 = (struct cv_line *) ptr;

             l = lf->lines_head;

             /* Only the bottom 24 bits of line_no actually encode the
                line number.  The top bit is a flag meaning "is
                a statement".  */

             bfd_putl32 (l->frag_offset - off, &l2->offset);
             bfd_putl32 (0x80000000 | (l->lineno & 0xffffff),
                         &l2->line_no);

             lf->lines_head = l->next;

             free(l);

             ptr += sizeof (struct cv_line);
           }

         lb->files_head = lf->next;
         free (lf);
       }

     blocks_head = lb->next;

     free (lb);
   }
}

/* Return the CodeView constant for the selected architecture.  */
static uint16_t
target_processor (void)
{
 switch (stdoutput->arch_info->arch)
   {
   case bfd_arch_i386:
     if (stdoutput->arch_info->mach & bfd_mach_x86_64)
       return CV_CFL_X64;
     else
       return CV_CFL_80386;

   case bfd_arch_aarch64:
     return CV_CFL_ARM64;

   default:
     return 0;
   }
}

/* Write the CodeView symbols, describing the object name and
  assembler version.  */
static void
write_symbols_info (void)
{
 static const char assembler[] = "GNU AS " VERSION;

 char *path = lrealpath (out_file_name);
 char *path2 = remap_debug_filename (path);
 size_t path_len, padding;
 uint32_t len;
 struct OBJNAMESYM objname;
 struct COMPILESYM3 compile3;
 char *ptr;

 free (path);
 path = path2;

 path_len = strlen (path);

 len = sizeof (struct OBJNAMESYM) + path_len + 1;
 len += sizeof (struct COMPILESYM3) + sizeof (assembler);

 if (len % 4)
   padding = 4 - (len % 4);
 else
   padding = 0;

 len += padding;

 ptr = frag_more (sizeof (uint32_t) + sizeof (uint32_t) + len);

 bfd_putl32 (DEBUG_S_SYMBOLS, ptr);
 ptr += sizeof (uint32_t);
 bfd_putl32 (len, ptr);
 ptr += sizeof (uint32_t);

 /* Write S_OBJNAME entry.  */

 bfd_putl16 (sizeof (struct OBJNAMESYM) - sizeof (uint16_t) + path_len + 1,
             &objname.length);
 bfd_putl16 (S_OBJNAME, &objname.type);
 bfd_putl32 (0, &objname.signature);

 memcpy (ptr, &objname, sizeof (struct OBJNAMESYM));
 ptr += sizeof (struct OBJNAMESYM);
 memcpy (ptr, path, path_len + 1);
 ptr += path_len + 1;

 free (path);

 /* Write S_COMPILE3 entry.  */

 bfd_putl16 (sizeof (struct COMPILESYM3) - sizeof (uint16_t)
             + sizeof (assembler) + padding, &compile3.length);
 bfd_putl16 (S_COMPILE3, &compile3.type);
 bfd_putl32 (CV_CFL_MASM, &compile3.flags);
 bfd_putl16 (target_processor (), &compile3.machine);
 bfd_putl16 (0, &compile3.frontend_major);
 bfd_putl16 (0, &compile3.frontend_minor);
 bfd_putl16 (0, &compile3.frontend_build);
 bfd_putl16 (0, &compile3.frontend_qfe);
 bfd_putl16 (0, &compile3.backend_major);
 bfd_putl16 (0, &compile3.backend_minor);
 bfd_putl16 (0, &compile3.backend_build);
 bfd_putl16 (0, &compile3.backend_qfe);

 memcpy (ptr, &compile3, sizeof (struct COMPILESYM3));
 ptr += sizeof (struct COMPILESYM3);
 memcpy (ptr, assembler, sizeof (assembler));
 ptr += sizeof (assembler);

 memset (ptr, 0, padding);
}

/* Processing of the file has finished, emit the .debug$S section.  */
void
codeview_finish (void)
{
 segT seg;

 if (!blocks_head)
   return;

 seg = subseg_new (".debug$S", 0);

 bfd_set_section_flags (seg, SEC_READONLY | SEC_NEVER_LOAD);

 bfd_putl32 (CV_SIGNATURE_C13, frag_more (sizeof (uint32_t)));

 write_string_table ();
 write_checksums ();
 write_lines_info ();
 write_symbols_info ();
}

/* Assign a new index number for the given file, or return the existing
  one if already assigned.  */
static unsigned int
get_fileno (const char *file)
{
 struct source_file *sf;
 char *path = lrealpath (file);
 char *path2 = remap_debug_filename (path);
 size_t path_len;
 FILE *f;

 free (path);
 path = path2;

 path_len = strlen (path);

 for (sf = files_head; sf; sf = sf->next)
   {
     if (path_len == strlen (sf->filename)
         && !filename_ncmp (sf->filename, path, path_len))
       {
         free (path);
         return sf->num;
       }
   }

 sf = xmalloc (sizeof (struct source_file));

 sf->next = NULL;
 sf->num = num_source_files;
 sf->filename = path;

 f = fopen (file, "r");
 if (!f)
   as_fatal (_("could not open %s for reading"), file);

 if (md5_stream (f, sf->md5))
   {
     fclose(f);
     as_fatal (_("md5_stream failed"));
   }

 fclose(f);

 if (!files_head)
   files_head = sf;
 else
   files_tail->next = sf;

 files_tail = sf;

 num_source_files++;

 return num_source_files - 1;
}

/* Called for each new line in asm file.  */
void
codeview_generate_asm_lineno (void)
{
 const char *file;
 unsigned int filenr;
 unsigned int lineno;
 struct line *l;
 symbolS *sym = NULL;
 struct line_block *lb;
 struct line_file *lf;

 file = as_where (&lineno);

 filenr = get_fileno (file);

 if (!blocks_tail || blocks_tail->frag != frag_now)
   {
     static int label_num = 0;
     char name[32];

     sprintf (name, ".Loc.%u", label_num);
     label_num++;
     sym = symbol_new (name, now_seg, frag_now, frag_now_fix ());

     lb = xmalloc (sizeof (struct line_block));
     lb->next = NULL;
     lb->seg = now_seg;
     lb->subseg = now_subseg;
     lb->frag = frag_now;
     lb->sym = sym;
     lb->files_head = lb->files_tail = NULL;

     if (!blocks_head)
       blocks_head = lb;
     else
       blocks_tail->next = lb;

     blocks_tail = lb;
   }
 else
   {
     lb = blocks_tail;
   }

 if (!lb->files_tail || lb->files_tail->fileno != filenr)
   {
     lf = xmalloc (sizeof (struct line_file));
     lf->next = NULL;
     lf->fileno = filenr;
     lf->lines_head = lf->lines_tail = NULL;
     lf->num_lines = 0;

     if (!lb->files_head)
       lb->files_head = lf;
     else
       lb->files_tail->next = lf;

     lb->files_tail = lf;
   }
 else
   {
     lf = lb->files_tail;
   }

 l = xmalloc (sizeof (struct line));
 l->next = NULL;
 l->lineno = lineno;
 l->frag_offset = frag_now_fix ();

 if (!lf->lines_head)
   lf->lines_head = l;
 else
   lf->lines_tail->next = l;

 lf->lines_tail = l;
 lf->num_lines++;
}

#else

void
codeview_finish (void)
{
}

void
codeview_generate_asm_lineno (void)
{
}

#endif /* TE_PE && O_secrel */