/* BFD back-end for PDB Multi-Stream Format archives.
   Copyright (C) 2022-2024 Free Software Foundation, Inc.

   This file is part of BFD, the Binary File Descriptor library.

   This program 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.

   This program 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.  */

/* This describes the MSF file archive format, which is used for the
   PDB debug info generated by MSVC. See https://llvm.org/docs/PDB/MsfFile.html
   for a full description of the format.  */

#include "sysdep.h"
#include "bfd.h"
#include "libbfd.h"

/* "Microsoft C/C++ MSF 7.00\r\n\x1a\x44\x53\0\0\0" */
static const uint8_t pdb_magic[] =
{ 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66,
  0x74, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20,
  0x4d, 0x53, 0x46, 0x20, 0x37, 0x2e, 0x30, 0x30,
  0x0d, 0x0a, 0x1a, 0x44, 0x53, 0x00, 0x00, 0x00 };

#define arch_eltdata(bfd) ((struct areltdata *) ((bfd)->arelt_data))

static bfd_cleanup
pdb_archive_p (bfd *abfd)
{
  int ret;
  char magic[sizeof (pdb_magic)];

  ret = bfd_read (magic, sizeof (magic), abfd);
  if (ret != sizeof (magic))
    {
      bfd_set_error (bfd_error_wrong_format);
      return NULL;
    }

  if (memcmp (magic, pdb_magic, sizeof (magic)))
    {
      bfd_set_error (bfd_error_wrong_format);
      return NULL;
    }

  void *tdata = bfd_zalloc (abfd, sizeof (struct artdata));
  if (tdata == NULL)
    return NULL;
  bfd_ardata (abfd) = tdata;

  return _bfd_no_cleanup;
}

static bfd *
pdb_get_elt_at_index (bfd *abfd, symindex sym_index)
{
  char int_buf[sizeof (uint32_t)];
  uint32_t block_size, block_map_addr, block, num_files;
  uint32_t first_dir_block, dir_offset, file_size, block_off, left;
  char name[10];
  bfd *file;
  char *buf;

  /* Get block_size.  */

  if (bfd_seek (abfd, sizeof (pdb_magic), SEEK_SET))
    return NULL;

  if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    {
      bfd_set_error (bfd_error_malformed_archive);
      return NULL;
    }

  block_size = bfd_getl32 (int_buf);
  if ((block_size & -block_size) != block_size
      || block_size < 512
      || block_size > 4096)
    {
      bfd_set_error (bfd_error_malformed_archive);
      return NULL;
    }

  /* Get block_map_addr.  */

  if (bfd_seek (abfd, 4 * sizeof (uint32_t), SEEK_CUR))
    return NULL;

  if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    {
      bfd_set_error (bfd_error_malformed_archive);
      return NULL;
    }

  block_map_addr = bfd_getl32 (int_buf);

  /* Get num_files.  */

  if (bfd_seek (abfd, block_map_addr * block_size, SEEK_SET))
    return NULL;

  if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    {
      bfd_set_error (bfd_error_malformed_archive);
      return NULL;
    }

  first_dir_block = bfd_getl32 (int_buf);

  if (bfd_seek (abfd, first_dir_block * block_size, SEEK_SET))
    return NULL;

  if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    {
      bfd_set_error (bfd_error_malformed_archive);
      return NULL;
    }

  num_files = bfd_getl32 (int_buf);

  if (sym_index >= num_files)
    {
      bfd_set_error (bfd_error_no_more_archived_files);
      return NULL;
    }

  /* Read file size.  */

  dir_offset = sizeof (uint32_t) * (sym_index + 1);

  if (dir_offset >= block_size)
    {
      uint32_t block_map_addr_off;

      block_map_addr_off = ((dir_offset / block_size) * sizeof (uint32_t));

      if (bfd_seek (abfd, (block_map_addr * block_size) + block_map_addr_off,
		    SEEK_SET))
	return NULL;

      if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
	{
	  bfd_set_error (bfd_error_malformed_archive);
	  return NULL;
	}

      block = bfd_getl32 (int_buf);
    }
  else
    {
      block = first_dir_block;
    }

  if (bfd_seek (abfd, (block * block_size) + (dir_offset % block_size),
		SEEK_SET))
    return NULL;

  if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    {
      bfd_set_error (bfd_error_malformed_archive);
      return NULL;
    }

  file_size = bfd_getl32 (int_buf);

  /* Undocumented? Seen on PDBs created by MSVC 2022.  */
  if (file_size == 0xffffffff)
    file_size = 0;

  /* Create BFD. */

  /* Four hex digits is enough - even though MSF allows for 32 bits, the
     PDB format itself only uses 16 bits for stream numbers.  */
  sprintf (name, "%04lx", sym_index);

  file = bfd_create (name, abfd);

  if (!file)
    return NULL;

  if (!bfd_make_writable (file))
    goto fail;

  file->arelt_data =
    (struct areltdata *) bfd_zmalloc (sizeof (struct areltdata));

  if (!file->arelt_data)
    goto fail;

  arch_eltdata (file)->parsed_size = file_size;
  arch_eltdata (file)->key = sym_index;

  if (file_size == 0)
    return file;

  block_off = 0;

  /* Sum number of blocks in previous files.  */

  if (sym_index != 0)
    {
      dir_offset = sizeof (uint32_t);

      if (bfd_seek (abfd, (first_dir_block * block_size) + sizeof (uint32_t),
		    SEEK_SET))
	goto fail;

      for (symindex i = 0; i < sym_index; i++)
	{
	  uint32_t size, num_blocks;

	  if ((dir_offset % block_size) == 0)
	    {
	      uint32_t block_map_addr_off;

	      block_map_addr_off =
		((dir_offset / block_size) * sizeof (uint32_t));

	      if (bfd_seek
		  (abfd, (block_map_addr * block_size) + block_map_addr_off,
		   SEEK_SET))
		goto fail;

	      if (bfd_read (int_buf, sizeof (uint32_t), abfd) !=
		  sizeof (uint32_t))
		{
		  bfd_set_error (bfd_error_malformed_archive);
		  goto fail;
		}

	      block = bfd_getl32 (int_buf);

	      if (bfd_seek (abfd, block * block_size, SEEK_SET))
		goto fail;
	    }

	  if (bfd_read (int_buf, sizeof (uint32_t), abfd) !=
	      sizeof (uint32_t))
	    {
	      bfd_set_error (bfd_error_malformed_archive);
	      goto fail;
	    }

	  size = bfd_getl32 (int_buf);

	  if (size == 0xffffffff)
	    size = 0;

	  num_blocks = (size + block_size - 1) / block_size;
	  block_off += num_blocks;

	  dir_offset += sizeof (uint32_t);
	}
    }

  /* Read blocks, and write into new BFD.  */

  dir_offset = sizeof (uint32_t) * (num_files + block_off + 1);

  if (dir_offset >= block_size)
    {
      uint32_t block_map_addr_off;

      block_map_addr_off = ((dir_offset / block_size) * sizeof (uint32_t));

      if (bfd_seek (abfd, (block_map_addr * block_size) + block_map_addr_off,
		    SEEK_SET))
	goto fail;

      if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
	{
	  bfd_set_error (bfd_error_malformed_archive);
	  goto fail;
	}

      block = bfd_getl32 (int_buf);
    }
  else
    {
      block = first_dir_block;
    }

  buf = bfd_malloc (block_size);
  if (!buf)
    goto fail;

  left = file_size;
  do
    {
      uint32_t file_block, to_read;

      if ((dir_offset % block_size) == 0 && left != file_size)
	{
	  uint32_t block_map_addr_off;

	  block_map_addr_off =
	    ((dir_offset / block_size) * sizeof (uint32_t));

	  if (bfd_seek
	      (abfd, (block_map_addr * block_size) + block_map_addr_off,
	       SEEK_SET))
	    goto fail2;

	  if (bfd_read (int_buf, sizeof (uint32_t), abfd) !=
	      sizeof (uint32_t))
	    {
	      bfd_set_error (bfd_error_malformed_archive);
	      goto fail2;
	    }

	  block = bfd_getl32 (int_buf);
	}

      if (bfd_seek (abfd, (block * block_size) + (dir_offset % block_size),
		    SEEK_SET))
	goto fail2;

      if (bfd_read (int_buf, sizeof (uint32_t), abfd) != sizeof (uint32_t))
	{
	  bfd_set_error (bfd_error_malformed_archive);
	  goto fail2;
	}

      file_block = bfd_getl32 (int_buf);

      if (bfd_seek (abfd, file_block * block_size, SEEK_SET))
	goto fail2;

      to_read = left > block_size ? block_size : left;

      if (bfd_read (buf, to_read, abfd) != to_read)
	{
	  bfd_set_error (bfd_error_malformed_archive);
	  goto fail2;
	}

      if (bfd_write (buf, to_read, file) != to_read)
	goto fail2;

      if (left > block_size)
	left -= block_size;
      else
	break;

      dir_offset += sizeof (uint32_t);
    }
  while (left > 0);

  free (buf);

  return file;

fail2:
  free (buf);

fail:
  bfd_close (file);
  return NULL;
}

static bfd *
pdb_openr_next_archived_file (bfd *archive, bfd *last_file)
{
  if (!last_file)
    return pdb_get_elt_at_index (archive, 0);
  else
    return pdb_get_elt_at_index (archive, arch_eltdata (last_file)->key + 1);
}

static int
pdb_generic_stat_arch_elt (bfd *abfd, struct stat *buf)
{
  buf->st_mtime = 0;
  buf->st_uid = 0;
  buf->st_gid = 0;
  buf->st_mode = 0644;
  buf->st_size = arch_eltdata (abfd)->parsed_size;

  return 0;
}

static uint32_t
pdb_allocate_block (uint32_t *num_blocks, uint32_t block_size)
{
  uint32_t block;

  block = *num_blocks;

  (*num_blocks)++;

  /* If new interval, skip two blocks for free space map.  */

  if ((block % block_size) == 1)
    {
      block += 2;
      (*num_blocks) += 2;
    }

  return block;
}

static bool
pdb_write_directory (bfd *abfd, uint32_t block_size, uint32_t num_files,
		     uint32_t block_map_addr, uint32_t * num_blocks)
{
  char tmp[sizeof (uint32_t)];
  uint32_t block, left, block_map_off;
  bfd *arelt;
  char *buf;

  /* Allocate first block for directory.  */

  block = pdb_allocate_block (num_blocks, block_size);
  left = block_size;

  /* Write allocated block no. at beginning of block map.  */

  if (bfd_seek (abfd, block_map_addr * block_size, SEEK_SET))
    return false;

  bfd_putl32 (block, tmp);

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  block_map_off = sizeof (uint32_t);

  /* Write num_files at beginning of directory.  */

  if (bfd_seek (abfd, block * block_size, SEEK_SET))
    return false;

  bfd_putl32 (num_files, tmp);

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  left -= sizeof (uint32_t);

  /* Write file sizes.  */

  arelt = abfd->archive_head;
  while (arelt)
    {
      if (left == 0)
	{
	  if (block_map_off == block_size) /* Too many blocks.  */
	    {
	      bfd_set_error (bfd_error_invalid_operation);
	      return false;
	    }

	  block = pdb_allocate_block (num_blocks, block_size);
	  left = block_size;

	  if (bfd_seek
	      (abfd, (block_map_addr * block_size) + block_map_off, SEEK_SET))
	    return false;

	  bfd_putl32 (block, tmp);

	  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
	    return false;

	  block_map_off += sizeof (uint32_t);

	  if (bfd_seek (abfd, block * block_size, SEEK_SET))
	    return false;
	}

      bfd_putl32 (bfd_get_size (arelt), tmp);

      if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
	return false;

      left -= sizeof (uint32_t);

      arelt = arelt->archive_next;
    }

  /* Write blocks.  */

  buf = bfd_malloc (block_size);
  if (!buf)
    return false;

  arelt = abfd->archive_head;
  while (arelt)
    {
      ufile_ptr size = bfd_get_size (arelt);
      uint32_t req_blocks = (size + block_size - 1) / block_size;

      if (bfd_seek (arelt, 0, SEEK_SET))
	{
	  free (buf);
	  return false;
	}

      for (uint32_t i = 0; i < req_blocks; i++)
	{
	  uint32_t file_block, to_read;

	  if (left == 0)
	    {
	      if (block_map_off == block_size) /* Too many blocks.  */
		{
		  bfd_set_error (bfd_error_invalid_operation);
		  free (buf);
		  return false;
		}

	      block = pdb_allocate_block (num_blocks, block_size);
	      left = block_size;

	      if (bfd_seek
		  (abfd, (block_map_addr * block_size) + block_map_off,
		   SEEK_SET))
		{
		  free (buf);
		  return false;
		}

	      bfd_putl32 (block, tmp);

	      if (bfd_write (tmp, sizeof (uint32_t), abfd) !=
		  sizeof (uint32_t))
		{
		  free (buf);
		  return false;
		}

	      block_map_off += sizeof (uint32_t);

	      if (bfd_seek (abfd, block * block_size, SEEK_SET))
		{
		  free (buf);
		  return false;
		}
	    }

	  /* Allocate block and write number into directory.  */

	  file_block = pdb_allocate_block (num_blocks, block_size);

	  bfd_putl32 (file_block, tmp);

	  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
	    {
	      free (buf);
	      return false;
	    }

	  left -= sizeof (uint32_t);

	  /* Read file contents into buffer.  */

	  to_read = size > block_size ? block_size : size;

	  if (bfd_read (buf, to_read, arelt) != to_read)
	    {
	      free (buf);
	      return false;
	    }

	  size -= to_read;

	  if (to_read < block_size)
	    memset (buf + to_read, 0, block_size - to_read);

	  if (bfd_seek (abfd, file_block * block_size, SEEK_SET))
	    {
	      free (buf);
	      return false;
	    }

	  /* Write file contents into allocated block.  */

	  if (bfd_write (buf, block_size, abfd) != block_size)
	    {
	      free (buf);
	      return false;
	    }

	  if (bfd_seek
	      (abfd, (block * block_size) + block_size - left, SEEK_SET))
	    {
	      free (buf);
	      return false;
	    }
	}

      arelt = arelt->archive_next;
    }

  memset (buf, 0, left);

  if (bfd_write (buf, left, abfd) != left)
    {
      free (buf);
      return false;
    }

  free (buf);

  return true;
}

static bool
pdb_write_bitmap (bfd *abfd, uint32_t block_size, uint32_t num_blocks)
{
  char *buf;
  uint32_t num_intervals = (num_blocks + block_size - 1) / block_size;

  buf = bfd_malloc (block_size);
  if (!buf)
    return false;

  num_blocks--;			/* Superblock not included.  */

  for (uint32_t i = 0; i < num_intervals; i++)
    {
      if (bfd_seek (abfd, ((i * block_size) + 1) * block_size, SEEK_SET))
	{
	  free (buf);
	  return false;
	}

      /* All of our blocks are contiguous, making our free block map simple.
         0 = used, 1 = free.  */

      if (num_blocks >= 8)
	memset (buf, 0,
		(num_blocks / 8) >
		block_size ? block_size : (num_blocks / 8));

      if (num_blocks < block_size * 8)
	{
	  unsigned int off = num_blocks / 8;

	  if (num_blocks % 8)
	    {
	      buf[off] = (1 << (8 - (num_blocks % 8))) - 1;
	      off++;
	    }

	  if (off < block_size)
	    memset (buf + off, 0xff, block_size - off);
	}

      if (num_blocks < block_size * 8)
	num_blocks = 0;
      else
	num_blocks -= block_size * 8;

      if (bfd_write (buf, block_size, abfd) != block_size)
	return false;
    }

  free (buf);

  return true;
}

static bool
pdb_write_contents (bfd *abfd)
{
  char tmp[sizeof (uint32_t)];
  const uint32_t block_size = 0x400;
  uint32_t block_map_addr;
  uint32_t num_blocks;
  uint32_t num_files = 0;
  uint32_t num_directory_bytes = sizeof (uint32_t);
  bfd *arelt;

  if (bfd_write (pdb_magic, sizeof (pdb_magic), abfd) != sizeof (pdb_magic))
    return false;

  bfd_putl32 (block_size, tmp);

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  bfd_putl32 (1, tmp); /* Free block map block (always either 1 or 2).  */

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  arelt = abfd->archive_head;

  while (arelt)
    {
      uint32_t blocks_required =
	(bfd_get_size (arelt) + block_size - 1) / block_size;

      num_directory_bytes += sizeof (uint32_t); /* Size.  */
      num_directory_bytes += blocks_required * sizeof (uint32_t); /* Blocks.  */

      num_files++;

      arelt = arelt->archive_next;
    }

  /* Superblock plus two bitmap blocks.  */
  num_blocks = 3;

  /* Skip num_blocks for now.  */
  if (bfd_seek (abfd, sizeof (uint32_t), SEEK_CUR))
    return false;

  bfd_putl32 (num_directory_bytes, tmp);

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  /* Skip unknown uint32_t (always 0?).  */
  if (bfd_seek (abfd, sizeof (uint32_t), SEEK_CUR))
    return false;

  block_map_addr = pdb_allocate_block (&num_blocks, block_size);

  bfd_putl32 (block_map_addr, tmp);

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  if (!pdb_write_directory
      (abfd, block_size, num_files, block_map_addr, &num_blocks))
    return false;

  if (!pdb_write_bitmap (abfd, block_size, num_blocks))
    return false;

  /* Write num_blocks now we know it.  */

  if (bfd_seek
      (abfd, sizeof (pdb_magic) + sizeof (uint32_t) + sizeof (uint32_t),
       SEEK_SET))
    return false;

  bfd_putl32 (num_blocks, tmp);

  if (bfd_write (tmp, sizeof (uint32_t), abfd) != sizeof (uint32_t))
    return false;

  return true;
}

#define pdb_bfd_free_cached_info _bfd_generic_bfd_free_cached_info
#define pdb_new_section_hook _bfd_generic_new_section_hook
#define pdb_get_section_contents _bfd_generic_get_section_contents
#define pdb_get_section_contents_in_window _bfd_generic_get_section_contents_in_window
#define pdb_close_and_cleanup _bfd_generic_close_and_cleanup

#define pdb_slurp_armap _bfd_noarchive_slurp_armap
#define pdb_slurp_extended_name_table _bfd_noarchive_slurp_extended_name_table
#define pdb_construct_extended_name_table _bfd_noarchive_construct_extended_name_table
#define pdb_truncate_arname _bfd_noarchive_truncate_arname
#define pdb_write_armap _bfd_noarchive_write_armap
#define pdb_read_ar_hdr _bfd_noarchive_read_ar_hdr
#define pdb_write_ar_hdr _bfd_noarchive_write_ar_hdr
#define pdb_update_armap_timestamp _bfd_noarchive_update_armap_timestamp

const bfd_target pdb_vec =
{
  "pdb",
  bfd_target_unknown_flavour,
  BFD_ENDIAN_LITTLE,		/* target byte order */
  BFD_ENDIAN_LITTLE,		/* target headers byte order */
  0,				/* object flags */
  0,				/* section flags */
  0,				/* leading underscore */
  ' ',				/* ar_pad_char */
  16,				/* ar_max_namelen */
  0,				/* match priority.  */
  TARGET_KEEP_UNUSED_SECTION_SYMBOLS, /* keep unused section symbols.  */
  bfd_getl64, bfd_getl_signed_64, bfd_putl64,
  bfd_getl32, bfd_getl_signed_32, bfd_putl32,
  bfd_getl16, bfd_getl_signed_16, bfd_putl16, /* Data.  */
  bfd_getl64, bfd_getl_signed_64, bfd_putl64,
  bfd_getl32, bfd_getl_signed_32, bfd_putl32,
  bfd_getl16, bfd_getl_signed_16, bfd_putl16, /* Hdrs.  */

  {				/* bfd_check_format */
    _bfd_dummy_target,
    _bfd_dummy_target,
    pdb_archive_p,
    _bfd_dummy_target
  },
  {				/* bfd_set_format */
    _bfd_bool_bfd_false_error,
    _bfd_bool_bfd_false_error,
    _bfd_bool_bfd_true,
    _bfd_bool_bfd_false_error
  },
  {				/* bfd_write_contents */
    _bfd_bool_bfd_true,
    _bfd_bool_bfd_false_error,
    pdb_write_contents,
    _bfd_bool_bfd_false_error
  },

  BFD_JUMP_TABLE_GENERIC (pdb),
  BFD_JUMP_TABLE_COPY (_bfd_generic),
  BFD_JUMP_TABLE_CORE (_bfd_nocore),
  BFD_JUMP_TABLE_ARCHIVE (pdb),
  BFD_JUMP_TABLE_SYMBOLS (_bfd_nosymbols),
  BFD_JUMP_TABLE_RELOCS (_bfd_norelocs),
  BFD_JUMP_TABLE_WRITE (_bfd_generic),
  BFD_JUMP_TABLE_LINK (_bfd_nolink),
  BFD_JUMP_TABLE_DYNAMIC (_bfd_nodynamic),

  NULL,

  NULL
};